<!-- Workflows Tab -->
<style>
  #wfModal {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    display: none;
    align-items: center;
    justify-content: center;
    z-index: 1000;
  }
  #wfModal[data-open="1"] {
    display: flex;
  }
  #wfModal_panel {
    background: var(--color-base-100);
    width: min(92vw, 960px);
    max-height: 92vh;
    border-radius: 12px;
    box-shadow: 0 16px 40px rgba(0, 0, 0, 0.4);
    display: flex;
    flex-direction: column;
    outline: none;
  }
  .wf-modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 14px 18px 4px 18px;
  }
  .wf-modal-header h3 {
    margin: 0;
    font-size: 1.1rem;
  }
  .wf-modal-body {
    padding: 12px 18px 18px 18px;
    overflow: auto;
    flex: 1 1 auto;
    min-height: 0;
  }
  .wf-modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    padding: 0 18px 16px 18px;
  }
  .wf-modal-body pre {
    white-space: pre-wrap;
    word-break: break-word;
    overflow: auto;
    background: var(--color-base-95);
    color: inherit;
    border-radius: 8px;
    padding: 12px;
    font-size: 13px;
    line-height: 1.4;
  }
  .visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  .wf-runs-container {
    margin-top: 8px;
    max-height: 520px;
    overflow: auto;
    border: 1px solid var(--color-base-40);
    border-radius: 10px;
    background: var(--color-base-100);
  }
  .wf-runs-table {
    width: 100%;
    border-collapse: collapse;
  }
  .wf-runs-table thead th {
    position: sticky;
    top: 0;
    background: var(--color-base-100);
    padding: 8px;
    border-bottom: 1px solid var(--color-base-50);
    z-index: 2;
    text-align: left;
  }
  .wf-runs-table tbody td {
    padding: 8px;
    border-bottom: 1px solid var(--color-base-30);
    vertical-align: middle;
  }
  .wf-runs-table tbody tr:hover td {
    background: var(--color-base-95);
  }
  .wf-run-status-chip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 2px 10px;
    border-radius: 999px;
    font-size: 12px;
    letter-spacing: 0.02em;
  }
  .wf-run-pad td {
    border: none !important;
    padding: 0 !important;
    background: transparent !important;
  }
  @keyframes wf-inline-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }
  .wf-inline-spinner {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    border: 2px solid var(--color-base-50);
    border-top-color: var(--color-accent-60, #0a84ff);
    animation: wf-inline-spin 0.9s linear infinite;
    display: inline-block;
    vertical-align: middle;
  }
  .wf-modal-toolbar {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
    margin-bottom: 8px;
  }
  .wf-modal-toolbar .api-button {
    font-size: 12px;
    padding: 4px 8px;
  }
  .wf-modal-toolbar .api-button.active {
    background: var(--color-base-90);
  }
  .wf-diff-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 12px;
    background: var(--color-base-98);
    border-radius: 6px;
    overflow: hidden;
  }
  .wf-diff-table th,
  .wf-diff-table td {
    border-bottom: 1px solid var(--color-base-40);
    padding: 6px 8px;
    text-align: left;
    vertical-align: top;
  }
  .wf-diff-table tr:last-child td {
    border-bottom: none;
  }
  .wf-diff-none {
    font-style: italic;
    color: var(--color-base-0);
  }
  .wf-webhook-status {
    margin-top: 6px;
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    align-items: center;
    font-size: 12px;
  }
  .wf-webhook-status-host {
    color: var(--color-base-0);
    font-style: italic;
  }
  .wf-webhook-status-code {
    font-family: monospace;
    color: var(--color-base-0);
  }
  .wf-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 2px 8px;
    border-radius: 10px;
    font-size: 11px;
    font-weight: 600;
    text-transform: lowercase;
    color: #fff;
  }
  .wf-badge.success { background: #2e7d32; }
  .wf-badge.warn { background: #f0ad4e; color: #1b1b1b; }
  .wf-badge.danger { background: #c62828; }
  .wf-badge.info { background: #0a84ff; }
  #wfEvents_table {
    max-height: 420px;
    overflow: auto;
    border: 1px solid var(--color-base-40);
    border-radius: 8px;
    background: var(--color-base-100);
    padding: 4px;
  }
  #wfEvents_table table {
    width: 100%;
    border-collapse: collapse;
  }
  #wfEvents_table thead th {
    position: sticky;
    top: 0;
    background: var(--color-base-100);
    z-index: 1;
  }
  #wfEvents_table td {
    vertical-align: top;
  }
  .wf-type-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 6px;
  }
  .wf-chip {
    display: inline-flex;
    align-items: center;
    padding: 4px 8px;
    border: 1px solid var(--color-base-40);
    border-radius: 12px;
    cursor: pointer;
    font-size: 12px;
    text-transform: lowercase;
    color: var(--color-base-0);
    transition: background 0.2s ease;
  }
  .wf-chip.active {
    background: var(--color-base-80);
    color: var(--color-base-100);
    border-color: var(--color-base-80);
  }
  .wf-delivery-table {
    width: 100%;
    border-collapse: collapse;
  }
  .wf-delivery-table th,
  .wf-delivery-table td {
    padding: 8px;
    border-bottom: 1px solid var(--color-base-40);
    text-align: left;
  }
  .wf-delivery-table tr:last-child td {
    border-bottom: none;
  }
  .wf-help {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--color-base-60);
    color: var(--color-base-100);
    font-size: 11px;
    font-weight: 600;
    cursor: help;
  }
</style>
<div id="tabWorkflowsDefinitions" class="tab-content">
  <h2>Workflows: Definitions</h2>
  <div class="card" style="margin-bottom:12px; padding:10px; border:1px solid var(--color-base-40); border-radius:8px;">
    <div style="display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;">
      <div>
        <h3 style="margin:0 0 6px 0">Configuration Cheat Sheet</h3>
        <div style="color:var(--color-base-0); font-size:0.9em">Read-only snapshot of effective Workflows settings</div>
      </div>
      <div>
        <button class="api-button" id="wfCfg_refresh">Refresh</button>
      </div>
    </div>
    <div id="wfConfig_card" style="margin-top:8px"></div>
  </div>
  <div class="form-group">
    <button class="api-button" id="wfStepTypes_btn">Get Step Types</button>
  </div>
  <div class="form-group">
    <label for="wfDef_payload">Definition JSON</label>
    <div id="wfTpl_featured" style="display:flex; gap:6px; align-items:center; flex-wrap:wrap; margin:6px 0">
      <span style="opacity:0.85">Featured:</span>
      <button class="api-button btn-sm" data-action="wfTpl-load" data-template="site_watch_digest_tts">Site Watch + Digest + TTS</button>
      <button class="api-button btn-sm" data-action="wfTpl-run" data-template="site_watch_digest_tts">Create & Run</button>
      <button class="api-button btn-sm" data-action="wfTpl-runwatch" data-template="site_watch_digest_tts">Run & Watch</button>
      <button class="api-button btn-sm" data-action="wfTpl-load" data-template="pdf_qa">PDF → Q&A</button>
      <button class="api-button btn-sm" data-action="wfTpl-run" data-template="pdf_qa">Create & Run</button>
      <button class="api-button btn-sm" data-action="wfTpl-runwatch" data-template="pdf_qa">Run & Watch</button>
      <button class="api-button btn-sm" data-action="wfTpl-load" data-template="paper_roundup">Paper Roundup</button>
      <button class="api-button btn-sm" data-action="wfTpl-run" data-template="paper_roundup">Create & Run</button>
      <button class="api-button btn-sm" data-action="wfTpl-runwatch" data-template="paper_roundup">Run & Watch</button>
      <button class="api-button btn-sm" data-action="wfTpl-load" data-template="policy_checker">Policy Checker</button>
      <button class="api-button btn-sm" data-action="wfTpl-run" data-template="policy_checker">Create & Run</button>
      <button class="api-button btn-sm" data-action="wfTpl-runwatch" data-template="policy_checker">Run & Watch</button>
      <button class="api-button btn-sm" data-action="wfTpl-load" data-template="podcast_summary_narration">Podcast → Summary → TTS</button>
      <button class="api-button btn-sm" data-action="wfTpl-run" data-template="podcast_summary_narration">Create & Run</button>
      <button class="api-button btn-sm" data-action="wfTpl-runwatch" data-template="podcast_summary_narration">Run & Watch</button>
    </div>
    <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin:6px 0">
      <label style="margin:0">Templates</label>
      <input id="wfTpl_search" type="search" placeholder="Search templates..." style="min-width:200px" />
      <label style="margin:0 0 0 6px">Tag</label>
      <select id="wfTpl_tag" style="min-width:160px"><option value="">(all)</option></select>
      <select id="wfTpl_select" style="min-width:240px"></select>
      <button class="api-button" id="wfTpl_search_btn">Search/Refresh</button>
      <button class="api-button" id="wfTpl_apply_btn">Load Selected</button>
      <button class="api-button" id="wfTpl_insert_btn">Insert Selected</button>
      <button class="api-button" id="wfTpl_run_btn">Run This Template</button>
      <button class="api-button" id="wfTpl_runwatch_btn">Run & Watch</button>
      <button class="api-button" id="wfTpl_saveas_btn" title="Save the editor JSON as a local template stored in your browser">Save as New</button>
      <button class="api-button btn-sm" id="wfTpl_delete_local_btn" title="Delete the selected local template">Delete Local</button>
      <button class="api-button" id="wfTpl_copy_curl_btn" title="Copy a shell snippet that uses jq to parse the workflow id and start a run">Copy cURL (Create+Run)</button>
      <label style="display:inline-flex; align-items:center; gap:6px; margin-left:6px; color:var(--color-base-0); font-size:0.9em" title="jq is used to extract the workflow id from the create response. Toggle 'No jq' for a placeholder-based script.">
        <span id="wfTpl_curl_note" title="Default: jq required. Switch to 'No jq' for a manual WID placeholder.">Requires jq</span>
        <span class="wf-help" title="The generated shell snippet uses jq to parse JSON responses. Toggle 'No jq' to output a placeholder-based script if jq is unavailable.">i</span>
        <span style="opacity:0.8">|</span>
        <input type="checkbox" id="wfTpl_curl_nojq" aria-label="Generate cURL snippet without jq" title="Generate a pure-cURL snippet without jq; you'll set WID manually"> No jq
      </label>
      <button class="api-button" id="wfTpl_reset_filters" title="Clears both search and tag filters and reloads the templates list">Reset filters</button>
    </div>
    <textarea id="wfDef_payload" rows="12" style="width:100%">{
  "name": "hello",
  "version": 1,
  "steps": [
    {"id": "s1", "type": "prompt", "config": {"template": "Hello {{ inputs.name }}", "save_artifact": true}}
  ]
}</textarea>
  </div>
  <div class="card" style="margin:12px 0; padding:10px; border:1px solid var(--color-base-40); border-radius:8px;">
    <div style="display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;">
      <div>
        <h3 style="margin:0 0 6px 0">Routing Helpers</h3>
        <div style="color:var(--color-base-0); font-size:0.9em">Wire success/failure paths without editing JSON manually.</div>
      </div>
      <div style="display:flex; gap:8px;">
        <button class="api-button btn-sm" id="wfRouting_refresh_ids" title="Rebuild the step lists from the JSON definition">Refresh Step IDs</button>
        <button class="api-button btn-sm" id="wfRouting_clear" title="Clear the current routing inputs">Clear</button>
      </div>
    </div>
    <div class="form-group" style="display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-top:10px;">
      <label for="wfRouting_step" style="margin:0;">Step</label>
      <input list="wfRouting_step_ids" id="wfRouting_step" placeholder="step id (e.g., prompt_1)" style="min-width:180px" />
      <label for="wfRouting_success" style="margin:0;">On success</label>
      <input list="wfRouting_target_ids" id="wfRouting_success" placeholder="next step id" style="min-width:180px" />
      <label for="wfRouting_failure" style="margin:0;">On failure</label>
      <input list="wfRouting_target_ids" id="wfRouting_failure" placeholder="fallback id (optional)" style="min-width:180px" />
      <button class="api-button" id="wfRouting_apply" title="Apply routing to the selected step">Apply Routing</button>
    </div>
    <div style="font-size:0.85em; color:var(--color-base-0); margin-top:6px;">
      Tip: Use the Branch step template to produce ready-made true/false paths.
    </div>
  </div>
  <datalist id="wfRouting_step_ids"></datalist>
  <datalist id="wfRouting_target_ids"></datalist>
  <div class="card" style="margin:12px 0; padding:10px; border:1px solid var(--color-base-40); border-radius:8px;">
    <div style="display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;">
      <div>
        <h3 style="margin:0 0 6px 0">Scheduler (workflow_run)</h3>
        <div style="color:var(--color-base-0); font-size:0.9em">Create cron schedules that enqueue workflow runs</div>
      </div>
      <div>
        <button class="api-button" id="wfSched_refresh">Refresh</button>
      </div>
    </div>
    <div class="form-group" style="display:flex; gap:8px; flex-wrap:wrap; align-items:center">
      <input type="text" id="wfSched_workflow_id" placeholder="workflow_id (optional)" style="width:140px"/>
      <input type="text" id="wfSched_name" placeholder="name (optional)" style="width:160px"/>
      <input type="text" id="wfSched_cron" placeholder="cron (e.g., */15 * * * *)" style="width:200px"/>
      <input type="text" id="wfSched_tz" placeholder="timezone (optional)" style="width:160px"/>
      <select id="wfSched_mode">
        <option value="async">async</option>
        <option value="sync">sync</option>
      </select>
      <div style="display:flex; align-items:center; gap:4px">
        <select id="wfSched_validation">
          <option value="block">block</option>
          <option value="non-block">non-block</option>
        </select>
        <span class="wf-help" title="Block validation prevents runs when inputs fail checks; non-block records validation issues but allows the run to continue.">i</span>
      </div>
      <label style="display:flex; gap:6px; align-items:center"><input type="checkbox" id="wfSched_enabled" checked /> Enabled</label>
      <label style="display:flex; gap:6px; align-items:center" title="If enabled, run only when you are online (have an active session)"><input type="checkbox" id="wfSched_require_online" /> Require online</label>
      <input type="text" id="wfSched_inputs" placeholder='inputs JSON (optional)' style="flex:1; min-width:220px"/>
      <button class="api-button" id="wfSched_create">Create</button>
    </div>
    <div id="wfSched_list"></div>
  </div>
  <div class="form-group">
    <button class="api-button" id="wfDef_create">Create Definition</button>
    <button class="api-button" id="wfDef_list">List Definitions</button>
    <button class="api-button" data-action="wf-insert" data-kind="delay">Insert Delay Step</button>
    <button class="api-button" data-action="wf-insert" data-kind="log">Insert Log Step</button>
    <button class="api-button" data-action="wf-insert" data-kind="branch">Insert Branch Step</button>
    <button class="api-button" data-action="wf-insert" data-kind="prompt">Insert Prompt Step</button>
    <button class="api-button" data-action="wf-insert" data-kind="rag">Insert RAG Search Step</button>
    <button class="api-button" data-action="wf-insert" data-kind="ingest">Insert Media Ingest Step</button>
    <button class="api-button" data-action="wf-insert" data-kind="tts">Insert TTS Step</button>
    <button class="api-button" data-action="wf-insert" data-kind="process_media">Insert Process Media Step</button>
    <button class="api-button" data-action="wf-insert" data-kind="rss">Insert RSS Fetch Step</button>
    <button class="api-button" data-action="wf-insert" data-kind="embed">Insert Embed Step</button>
    <button class="api-button" data-action="wf-insert" data-kind="translate">Insert Translate Step</button>
    <button class="api-button" data-action="wf-insert" data-kind="stt">Insert STT Transcribe Step</button>
    <button class="api-button" data-action="wf-insert" data-kind="notify">Insert Notify Step</button>
    <button class="api-button" data-action="wf-insert" data-kind="diff">Insert Diff Change Step</button>
  </div>
  <div class="form-group">
    <label>Workflow ID</label>
    <input type="text" id="wfRun_workflow_id" placeholder="numeric id" />
    <label>Mode</label>
    <select id="wfRun_mode">
      <option value="async">async</option>
      <option value="sync">sync</option>
    </select>
    <label>Inputs (JSON)</label>
    <input type="text" id="wfRun_inputs" placeholder='{"name":"Alice"}' />
    <button class="api-button" id="wfRun_saved">Run Saved</button>
  </div>
  <div class="form-group" style="margin-bottom:4px">
    <button class="api-button" data-action="wf-copy-json" data-target="wfDef_result">Copy</button>
    <button class="api-button" data-action="wf-clear-json" data-target="wfDef_result">Clear</button>
  </div>
  <pre id="wfDef_result" class="json-viewer"></pre>
  <script>
  async function wfLoadConfig(){
    try{
      const cfg = await apiClient.makeRequest('GET','/api/v1/workflows/config');
      wfRenderConfigCard(cfg);
    }catch(e){ Toast.error('Failed to load config: '+(e?.message||e)); }
  }
  function _wfRow(label, value){
    const v = (Array.isArray(value) ? value.join(', ') : (value===true? 'true': value===false? 'false' : (value ?? '')));
    return `<div style=\"display:flex;gap:8px;justify-content:space-between;border-bottom:1px dashed var(--color-base-30);padding:4px 0\"><div style=\"color:var(--color-base-0)\">${label}</div><div style=\"font-family:monospace\">${String(v)}</div></div>`;
  }
  function wfRenderConfigCard(cfg){
    try{
      const c = document.getElementById('wfConfig_card');
      if (!cfg || typeof cfg !== 'object'){ c.innerHTML = '<div style="color:var(--color-base-0)">No data</div>'; return; }
      const backend = cfg.backend||{};
      const rl = cfg.rate_limits||{};
      const eng = cfg.engine||{};
      const eg = cfg.egress||{};
      const wh = cfg.webhooks||{};
      const art = cfg.artifacts||{};
      let html = '';
      html += '<div style="display:grid;grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));gap:12px">';
      html += `<div style="border:1px solid var(--color-base-30);border-radius:6px;padding:8px"><div style="font-weight:600;margin-bottom:4px">Backend</div>${_wfRow('type', backend.type||'(auto)')}</div>`;
      html += `<div style="border:1px solid var(--color-base-30);border-radius:6px;padding:8px"><div style="font-weight:600;margin-bottom:4px">Rate Limits / Quotas <span class=\"wf-help\" title=\"Burst per minute limits prevent floods; daily per user caps total runs a user can start each day.\">i</span></div>${_wfRow('limits_disabled', rl.disabled)}${_wfRow('quotas_disabled', rl.quotas_disabled)}${_wfRow('burst_per_min', rl.quota_burst_per_min)}${_wfRow('daily_per_user', rl.quota_daily_per_user)}</div>`;
      html += `<div style="border:1px solid var(--color-base-30);border-radius:6px;padding:8px"><div style="font-weight:600;margin-bottom:4px">Engine</div>${_wfRow('tenant_concurrency', eng.tenant_concurrency)}${_wfRow('workflow_concurrency', eng.workflow_concurrency)}</div>`;
      html += `<div style="border:1px solid var(--color-base-30);border-radius:6px;padding:8px"><div style="font-weight:600;margin-bottom:4px">Egress</div>${_wfRow('profile <span class=\"wf-help\" title=\"Egress profiles guard against SSRF by restricting outbound destinations. Choose a profile suited to your environment.\">i</span>', eg.profile)}${_wfRow('allowed_ports', eg.allowed_ports)}${_wfRow('allowlist', eg.allowlist)}${_wfRow('block_private', eg.block_private)}</div>`;
      html += `<div style="border:1px solid var(--color-base-30);border-radius:6px;padding:8px"><div style="font-weight:600;margin-bottom:4px">Webhooks</div>${_wfRow('completion_disabled', wh.completion_disabled)}${_wfRow('secret_set', wh.secret_set)}${_wfRow('dlq_enabled', wh.dlq_enabled)}${_wfRow('allowlist', wh.allowlist)}${_wfRow('denylist', wh.denylist)}</div>`;
      html += `<div style="border:1px solid var(--color-base-30);border-radius:6px;padding:8px"><div style="font-weight:600;margin-bottom:4px">Artifacts</div>${_wfRow('validate_strict', art.validate_strict)}${_wfRow('encryption_enabled', art.encryption_enabled)}${_wfRow('gc_enabled', art.gc_enabled)}${_wfRow('retention_days', art.retention_days)}</div>`;
      html += '</div>';
      c.innerHTML = html;
    }catch(e){ /* ignore */ }
  }
  async function wfCreateDefinition(){
    try{
      const body = JSON.parse(document.getElementById('wfDef_payload').value);
      const resp = await apiClient.makeRequest('POST','/api/v1/workflows',{ body });
      document.getElementById('wfDef_result').textContent = JSON.stringify(resp,null,2);
      Toast.success('Created definition id='+(resp?.id||''));
    }catch(e){ Toast.error('Create failed: '+(e?.message||e)); }
  }
  async function wfTplLoadList(){
    try{
      const sel = document.getElementById('wfTpl_select');
      sel.innerHTML = '';
      const q = (document.getElementById('wfTpl_search')?.value||'').trim();
      const tagSel = document.getElementById('wfTpl_tag');
      const tagVal = (tagSel?.value||'').trim();
      let url = '/api/v1/workflows/templates';
      const params = new URLSearchParams();
      if (q) params.set('q', q);
      if (tagVal) params.set('tag', tagVal);
      if ([...params.keys()].length){ url += '?' + params.toString(); }
      const items = await apiClient.makeRequest('GET', url);
      // persist filters
      wfTplSavePrefs();
      const localsRaw = wfTplGetLocalTemplates();
      const qLower = q ? q.toLowerCase() : null;
      const tagNorm = tagVal ? tagVal.toLowerCase() : null;
      const locals = localsRaw.filter(t=>{
        if (qLower && !t.name.toLowerCase().includes(qLower)) return false;
        if (tagNorm){
          const tags = Array.isArray(t.body?.tags) ? t.body.tags.map(s=>String(s).toLowerCase()) : [];
          if (!tags.includes(tagNorm)) return false;
        }
        return true;
      }).sort((a,b)=>a.name.localeCompare(b.name));
      if (locals.length){
        const grp = document.createElement('optgroup');
        grp.label = 'My Templates';
        locals.forEach(t=>{
          const opt = document.createElement('option');
          opt.value = `local::${t.name}`;
          const pretty = wfHumanTitle(t.name);
          opt.textContent = `${pretty} (local)`;
          grp.appendChild(opt);
        });
        sel.appendChild(grp);
      }
      const remoteItems = Array.isArray(items) ? items : [];
      if (remoteItems.length){
        const grpBuiltIn = document.createElement('optgroup');
        grpBuiltIn.label = 'Built-in';
        for (const it of remoteItems){
          const opt = document.createElement('option');
          opt.value = it.name;
          const pretty = wfHumanTitle(it.title || it.name);
          const label = (it.name && pretty && pretty !== it.name) ? `${pretty} - ${it.name}` : pretty || it.name;
          opt.textContent = label;
          grpBuiltIn.appendChild(opt);
        }
        sel.appendChild(grpBuiltIn);
      }
      if (!locals.length && !remoteItems.length){
        const opt = document.createElement('option'); opt.value=''; opt.textContent='(no templates found)'; sel.appendChild(opt);
      }
      // Restore last selection if present
      try{
        const saved = (typeof Utils !== 'undefined') ? Utils.getFromStorage('workflows-last-template') : null;
        if (saved){ sel.value = saved; }
      }catch(_){ }
      // Persist selection on change (attach once)
      try{
        if (!sel._wf_has_onchange){
          sel.addEventListener('change', ()=>{ try{ if (typeof Utils !== 'undefined') Utils.saveToStorage('workflows-last-template', sel.value); }catch(_){ } });
          sel._wf_has_onchange = true;
        }
      }catch(_){ }
      Toast.success('Templates loaded');
    }catch(e){ Toast.error('Failed to load templates: '+(e?.message||e)); }
  }
  function wfTplResetFilters(){
    try{
      const q = document.getElementById('wfTpl_search'); if (q) q.value = '';
      const tagSel = document.getElementById('wfTpl_tag'); if (tagSel) tagSel.value = '';
      wfTplSavePrefs();
      wfTplLoadList();
    }catch(_){ }
  }
  function wfHumanTitle(s){
    try{
      if (!s) return '';
      let t = String(s).replace(/[\-_]+/g, ' ').trim();
      // Title case words, preserving non-word tokens like arrows
      t = t.replace(/\b([a-z])(\w*)/g, (m,a,b)=> a.toUpperCase()+b.toLowerCase());
      return t;
    }catch(_){ return String(s); }
  }
  async function wfTplApply(){
    try{
      const sel = document.getElementById('wfTpl_select');
      const name = sel.value;
      if (!name){ Toast.error('Pick a template'); return; }
      try{ if (typeof Utils !== 'undefined') Utils.saveToStorage('workflows-last-template', name); }catch(_){ }
      const data = await wfTplResolve(name);
      if (!data) return;
      document.getElementById('wfDef_payload').value = JSON.stringify(data,null,2);
      Toast.success('Template loaded: '+name);
    }catch(e){ Toast.error('Failed to load template: '+(e?.message||e)); }
  }
  async function wfTplLoadByName(name){
    try{
      if (!name){ return; }
      const data = await wfTplResolve(name);
      if (!data) return;
      document.getElementById('wfDef_payload').value = JSON.stringify(data,null,2);
      Toast.success('Template loaded: '+wfHumanTitle(name));
    }catch(e){ Toast.error('Failed to load template: '+(e?.message||e)); }
  }
  async function wfTplInsert(){
    try{
      const sel = document.getElementById('wfTpl_select');
      const name = sel.value;
      if (!name){ Toast.error('Pick a template'); return; }
      try{ if (typeof Utils !== 'undefined') Utils.saveToStorage('workflows-last-template', name); }catch(_){ }
      const tpl = await wfTplResolve(name);
      if (!tpl) return;
      const ta = document.getElementById('wfDef_payload');
      const obj = (()=>{ try{ return JSON.parse(ta.value||'{}'); }catch(_){ return {}; } })();
      obj.name = obj.name || (tpl && tpl.name) || 'composed';
      obj.version = obj.version || 1;
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      if (Array.isArray(tpl?.steps)){
        for (const step of tpl.steps){ obj.steps.push(step); }
      }
      ta.value = JSON.stringify(obj, null, 2);
      Toast.success('Inserted steps from template: '+name);
    }catch(e){ Toast.error('Failed to insert: '+(e?.message||e)); }
  }
  async function wfTplRun(){
    try{
      const sel = document.getElementById('wfTpl_select');
      const name = sel.value;
      if (!name){ Toast.error('Pick a template'); return; }
      try{ if (typeof Utils !== 'undefined') Utils.saveToStorage('workflows-last-template', name); }catch(_){ }
      const tpl = await wfTplResolve(name);
      if (!tpl) return;
      await wfTplCreateAndRun(tpl, false);
    }catch(e){ Toast.error('Run template failed: '+(e?.message||e)); }
  }

  // Create+Run and automatically navigate and watch status
  async function wfTplRunWatch(){
    try{
      const sel = document.getElementById('wfTpl_select');
      const name = sel.value;
      if (!name){ Toast.error('Pick a template'); return; }
      try{ if (typeof Utils !== 'undefined') Utils.saveToStorage('workflows-last-template', name); }catch(_){ }
      const tpl = await wfTplResolve(name);
      if (!tpl) return;
      await wfTplCreateAndRun(tpl, true);
    }catch(e){ Toast.error('Run & Watch failed: '+(e?.message||e)); }
  }

  async function wfTplRunByName(name){
    try{
      if (!name){ return; }
      const tpl = await wfTplResolve(name);
      if (!tpl) return;
      await wfTplCreateAndRun(tpl, false);
    }catch(e){ Toast.error('Run featured failed: '+(e?.message||e)); }
  }
  async function wfTplRunWatchByName(name){
    try{
      if (!name){ return; }
      const tpl = await wfTplResolve(name);
      if (!tpl) return;
      await wfTplCreateAndRun(tpl, true);
    }catch(e){ Toast.error('Run & watch failed: '+(e?.message||e)); }
  }

  async function wfTplCopyCurl(){
    try{
      const sel = document.getElementById('wfTpl_select');
      const name = sel.value;
      if (!name){ Toast.error('Pick a template'); return; }
      const tpl = await wfTplResolve(name);
      if (!tpl) return;
      const base = (apiClient && apiClient.baseUrl) ? apiClient.baseUrl : 'http://localhost:8000';
      const token = (apiClient && apiClient.token) ? apiClient.token : 'YOUR_API_KEY_HERE';
      const json = JSON.stringify(tpl, null, 2);
      const snippet = `#!/usr/bin/env bash\nset -e\nBASE='${base}'\nAPI_KEY='${token}'\n\n# Requires: jq\ncat > /tmp/wf_def.json << 'JSON'\n${json}\nJSON\n\nCREATE=$(curl -sS -X POST "$BASE/api/v1/workflows" \\\n  -H "Content-Type: application/json" \\\n  -H "X-API-KEY: $API_KEY" \\\n  --data-binary @/tmp/wf_def.json)\necho "$CREATE" | jq .\nWID=$(echo "$CREATE" | jq -r '.id')\n\nRUN=$(curl -sS -X POST "$BASE/api/v1/workflows/$WID/run?mode=async" \\\n  -H "Content-Type: application/json" \\\n  -H "X-API-KEY: $API_KEY" \\\n  --data '{"inputs": {}}')\necho "$RUN" | jq .\n`;
      await navigator.clipboard.writeText(snippet);
      Toast.success('Copied cURL (create+run)');
    }catch(e){ Toast.error('Copy cURL failed: '+(e?.message||e)); }
  }

  // New generator supporting no-jq mode and using current API base/token
  async function wfTplCopyCurlAlt(){
    try{
      const sel = document.getElementById('wfTpl_select');
      const name = sel.value;
      if (!name){ Toast.error('Pick a template'); return; }
      const tpl = await wfTplResolve(name);
      if (!tpl) return;
      const base = (apiClient && apiClient.baseUrl) ? apiClient.baseUrl : 'http://localhost:8000';
      const token = (apiClient && apiClient.token) ? apiClient.token : 'YOUR_API_KEY_HERE';
      const json = JSON.stringify(tpl, null, 2);
      const nojq = !!document.getElementById('wfTpl_curl_nojq')?.checked;
      let snippet;
      if (!nojq){
        snippet = `#!/usr/bin/env bash\nset -e\nBASE='${base}'\nAPI_KEY='${token}'\n\n# Requires: jq\ncat > /tmp/wf_def.json << 'JSON'\n${json}\nJSON\n\nCREATE=$(curl -sS -X POST "$BASE/api/v1/workflows" \\\n+  -H "Content-Type: application/json" \\\n+  -H "X-API-KEY: $API_KEY" \\\n+  --data-binary @/tmp/wf_def.json)\necho "$CREATE" | jq .\nWID=$(echo "$CREATE" | jq -r '.id')\n\nRUN=$(curl -sS -X POST "$BASE/api/v1/workflows/$WID/run?mode=async" \\\n+  -H "Content-Type: application/json" \\\n+  -H "X-API-KEY: $API_KEY" \\\n+  --data '{"inputs": {}}')\necho "$RUN" | jq .\n`;
      } else {
        snippet = `#!/usr/bin/env bash\nset -e\nBASE='${base}'\nAPI_KEY='${token}'\n\ncat > /tmp/wf_def.json << 'JSON'\n${json}\nJSON\n\n# Create definition (response saved to file)\ncurl -sS -X POST "$BASE/api/v1/workflows" \\\n+  -H "Content-Type: application/json" \\\n+  -H "X-API-KEY: $API_KEY" \\\n+  --data-binary @/tmp/wf_def.json > /tmp/wf_created.json\n\necho "Created response saved to /tmp/wf_created.json"\necho "Open it and set WID manually:"\necho "export WID=REPLACE_WITH_ID"\n\n# Run (replace WID first)\ncurl -sS -X POST "$BASE/api/v1/workflows/$WID/run?mode=async" \\\n+  -H "Content-Type: application/json" \\\n+  -H "X-API-KEY: $API_KEY" \\\n+  --data '{"inputs": {}}' > /tmp/wf_run.json\n\necho "Run response saved to /tmp/wf_run.json"\n`;
      }
      await navigator.clipboard.writeText(snippet);
      Toast.success('Copied cURL (create+run)');
    }catch(e){ Toast.error('Copy cURL failed: '+(e?.message||e)); }
  }

  function wfTplCurlToggle(){
    try{
      const chk = document.getElementById('wfTpl_curl_nojq');
      const note = document.getElementById('wfTpl_curl_note');
      if (!note || !chk) return;
      note.textContent = chk.checked ? 'No jq mode' : 'Requires jq';
    }catch(_){ }
  }

  // Simple run status watcher for the Runs tab
  let wfStatusTimer = null;
  let wfStatusRunId = null;
  let wfWatchInternalUpdate = false;
  function wfSetWatchSpinner(active){
    try{
      const spinner = document.getElementById('wfWatch_spinner');
      if (spinner){
        spinner.style.display = active ? 'inline-block' : 'none';
        spinner.setAttribute('aria-hidden', active ? 'false' : 'true');
      }
    }catch(_){ }
  }
  function wfSetWatchToggle(checked){
    try{
      const toggle = document.getElementById('wfWatch_toggle');
      if (!toggle || toggle.checked === checked) return;
      wfWatchInternalUpdate = true;
      toggle.checked = checked;
      setTimeout(()=>{ wfWatchInternalUpdate = false; }, 0);
    }catch(_){
      wfWatchInternalUpdate = false;
    }
  }
  function wfToggleWatchStatus(){
    try{
      if (wfWatchInternalUpdate) return;
      const toggle = document.getElementById('wfWatch_toggle');
      if (!toggle) return;
      if (toggle.checked){
        const runId = (document.getElementById('wfRun_run_id')?.value||'').trim();
        if (!runId){
          Toast.error('Enter run id to watch');
          wfWatchInternalUpdate = true;
          toggle.checked = false;
          setTimeout(()=>{ wfWatchInternalUpdate = false; },0);
          return;
        }
        wfStartWatchStatus(runId);
      } else {
        wfStopWatchStatus();
      }
    }catch(_){ }
  }
  function wfStartWatchStatus(runId){
    try{
      if (wfStatusTimer){ clearInterval(wfStatusTimer); wfStatusTimer = null; }
      wfStatusRunId = runId;
      try{ const inp=document.getElementById('wfRun_run_id'); if (inp){ inp.value = String(runId||''); } }catch(_){ }
      wfStatusTimer = setInterval(async ()=>{
        try{
          const r = await apiClient.makeRequest('GET',`/api/v1/workflows/runs/${encodeURIComponent(wfStatusRunId)}`);
          set('wfRun_result', r);
          const st = (r && r.status) ? String(r.status) : '';
          if (st && (st === 'succeeded' || st === 'failed' || st === 'cancelled')){
            clearInterval(wfStatusTimer); wfStatusTimer = null;
            wfSetWatchSpinner(false);
            wfSetWatchToggle(false);
            Toast.info(`Run ${st}`);
          }
        }catch(e){ /* ignore transient */ }
      }, 1500);
      // Optionally enable auto events
      try{
        const chk = document.getElementById('wfEvents_auto');
        if (chk && !chk.checked){ chk.checked = true; wfToggleAutoEvents(); }
      }catch(_){ }
      // Cleanup on unload
      window.addEventListener('beforeunload', ()=>{ try{ if (wfStatusTimer) clearInterval(wfStatusTimer); }catch(_){ } });
      wfSetWatchToggle(true);
      wfSetWatchSpinner(true);
    }catch(_){ }
  }
  function wfStopWatchStatus(){
    try{
      if (wfStatusTimer){ clearInterval(wfStatusTimer); wfStatusTimer = null; Toast.info('Stopped watching'); }
      // Also stop auto events refresh if enabled
      try{ const chk=document.getElementById('wfEvents_auto'); if (chk && chk.checked){ chk.checked=false; wfToggleAutoEvents(); } }catch(_){ }
      wfSetWatchSpinner(false);
      wfSetWatchToggle(false);
    }catch(_){ }
  }

  // Navigate to the Workflows → Runs tab and pre-fill the run ID
  function wfGoToRun(runId){
    try{
      const topBtn = document.querySelector('.top-tab-button[data-toptab="workflows"]');
      const doFill = () => { setTimeout(()=>{ try{ const inp=document.getElementById('wfRun_run_id'); if (inp){ inp.value = String(runId||''); } }catch(_){ } }, 150); };
      if (window.webUI && topBtn){
        // Activate Workflows tab, then the Runs sub-tab
        Promise.resolve(webUI.activateTopTab(topBtn)).then(()=>{
          try{ const runsBtn = document.querySelector('#workflows-subtabs .sub-tab-button[data-content-id="tabWorkflowsRuns"]'); if (runsBtn) webUI.activateSubTab(runsBtn); }catch(_){ }
          doFill();
          // Follow-up toast with quick Get Status action
          setTimeout(()=>{
            try{
              const t = Toast.info('Ready to check status', 6000);
              const btn = document.createElement('button');
              btn.className = 'api-button btn-sm';
              btn.style.marginLeft = '8px';
              btn.textContent = 'Get Status';
              btn.onclick = ()=>{ try{ wfGetRun(); }catch(_){} };
              const content = t?.querySelector?.('.toast-content .toast-message') || t?.querySelector?.('.toast-content') || t;
              if (content) content.appendChild(btn);
            }catch(_){ }
          }, 220);
        });
      } else {
        // Fallback: show content directly
        try{
          document.querySelectorAll('.tab-content').forEach(el=>el.classList.remove('active'));
          const runs = document.getElementById('tabWorkflowsRuns'); if (runs) runs.classList.add('active');
        }catch(_){ }
        doFill();
        try{
          const t = Toast.info('Ready to check status', 6000);
          const btn = document.createElement('button');
          btn.className = 'api-button btn-sm';
          btn.style.marginLeft = '8px';
          btn.textContent = 'Get Status';
          btn.onclick = ()=>{ try{ wfGetRun(); }catch(_){} };
          const content = t?.querySelector?.('.toast-content .toast-message') || t?.querySelector?.('.toast-content') || t;
          if (content) content.appendChild(btn);
        }catch(_){ }
      }
    }catch(_){ }
  }
  async function wfListDefinitions(){
    try{
      const resp = await apiClient.makeRequest('GET','/api/v1/workflows');
      document.getElementById('wfDef_result').textContent = JSON.stringify(resp,null,2);
    }catch(e){ Toast.error('List failed: '+(e?.message||e)); }
  }
  async function wfGetStepTypes(){
    try{
      const resp = await apiClient.makeRequest('GET','/api/v1/workflows/step-types');
      document.getElementById('wfDef_result').textContent = JSON.stringify(resp,null,2);
    }catch(e){ Toast.error('Step types failed: '+(e?.message||e)); }
  }
  async function wfRunSaved(){
    try{
      const wid = document.getElementById('wfRun_workflow_id').value.trim();
      const mode = document.getElementById('wfRun_mode').value;
      const inputsText = document.getElementById('wfRun_inputs').value || '{}';
      const body = { inputs: JSON.parse(inputsText) };
      const resp = await apiClient.makeRequest('POST',`/api/v1/workflows/${encodeURIComponent(wid)}/run?mode=${encodeURIComponent(mode)}`,{ body });
      document.getElementById('wfDef_result').textContent = JSON.stringify(resp,null,2);
      Toast.success('Run started: '+(resp?.run_id||''));
    }catch(e){ Toast.error('Run failed: '+(e?.message||e)); }
  }
  async function wfSchedCreate(){
    try{
      const widRaw = document.getElementById('wfSched_workflow_id').value.trim();
      const name = document.getElementById('wfSched_name').value.trim() || null;
      const cron = document.getElementById('wfSched_cron').value.trim();
      const timezone = document.getElementById('wfSched_tz').value.trim() || null;
      const run_mode = document.getElementById('wfSched_mode').value;
      const validation_mode = document.getElementById('wfSched_validation').value;
      const enabled = document.getElementById('wfSched_enabled').checked;
      const require_online = document.getElementById('wfSched_require_online')?.checked || false;
      const inputsText = document.getElementById('wfSched_inputs').value.trim();
      const body = { name, cron, timezone, run_mode, validation_mode, enabled, require_online };
      if (widRaw) body.workflow_id = parseInt(widRaw,10);
      if (inputsText){ try{ body.inputs = JSON.parse(inputsText); }catch(e){ Toast.error('Invalid inputs JSON'); return; } }
      const resp = await apiClient.makeRequest('POST','/api/v1/scheduler/workflows',{ body });
      Toast.success('Created schedule '+(resp?.id||''));
      await wfSchedList();
    }catch(e){ Toast.error('Create schedule failed: '+(e?.message||e)); }
  }
  async function wfSchedList(){
    try{
      const resp = await apiClient.makeRequest('GET','/api/v1/scheduler/workflows');
      const host = document.getElementById('wfSched_list');
      host.innerHTML = wfSchedRender(resp||[]);
    }catch(e){ Toast.error('List schedules failed: '+(e?.message||e)); }
  }
  function wfSchedRender(items){
    if (!Array.isArray(items) || items.length===0){ return '<div style="color:var(--color-base-0)">No schedules</div>'; }
    let rows = '';
    for (const it of items){
      rows += `<tr>
        <td style="font-family:monospace">${it.id}</td>
        <td>${it.name||''}</td>
        <td>${String(it.workflow_id||'')}</td>
        <td><code>${it.cron}</code></td>
        <td>${it.timezone||''}</td>
        <td>${it.run_mode}</td>
        <td>${it.validation_mode}</td>
        <td>${it.enabled? 'yes':'no'}</td>
        <td>${it.require_online? 'yes':'no'}</td>
        <td>
          <button class="api-button btn-sm" onclick="wfSchedRunNow('${it.id}')">Run</button>
          <button class="api-button btn-sm" onclick="wfSchedToggle('${it.id}', ${it.enabled? 'false':'true'})">${it.enabled? 'Disable':'Enable'}</button>
          <button class="api-button btn-sm" onclick="wfSchedToggleOnline('${it.id}', ${it.require_online? 'false':'true'})">${it.require_online? 'Always-run':'Require-online'}</button>
          <button class="api-button btn-sm danger" onclick="wfSchedDelete('${it.id}')">Delete</button>
        </td>
      </tr>`;
    }
    return `<table class="table-compact"><thead><tr><th>ID</th><th>Name</th><th>WID</th><th>Cron</th><th>TZ</th><th>Mode</th><th>Validation</th><th>Enabled</th><th>Require Online</th><th>Actions</th></tr></thead><tbody>${rows}</tbody></table>`;
  }
  async function wfSchedDelete(id){
    try{
      await apiClient.makeRequest('DELETE',`/api/v1/scheduler/workflows/${encodeURIComponent(id)}`);
      await wfSchedList();
      Toast.success('Deleted');
    }catch(e){ Toast.error('Delete failed: '+(e?.message||e)); }
  }
  async function wfSchedToggle(id, enabled){
    try{
      const body = { enabled };
      await apiClient.makeRequest('PATCH',`/api/v1/scheduler/workflows/${encodeURIComponent(id)}`,{ body });
      await wfSchedList();
    }catch(e){ Toast.error('Toggle failed: '+(e?.message||e)); }
  }
  async function wfSchedRunNow(id){
    try{
      const r = await apiClient.makeRequest('POST',`/api/v1/scheduler/workflows/${encodeURIComponent(id)}/run-now`);
      Toast.success('Enqueued task '+(r?.task_id||''));
    }catch(e){ Toast.error('Run-now failed: '+(e?.message||e)); }
  }
  async function wfSchedToggleOnline(id, require_online){
    try{
      const body = { require_online };
      await apiClient.makeRequest('PATCH',`/api/v1/scheduler/workflows/${encodeURIComponent(id)}`,{ body });
      await wfSchedList();
    }catch(e){ Toast.error('Toggle require-online failed: '+(e?.message||e)); }
  }
  let wfRoutingRefreshTimer = null;
  function wfRoutingDebounceRefresh(){
    try{
      if(wfRoutingRefreshTimer){
        clearTimeout(wfRoutingRefreshTimer);
      }
      wfRoutingRefreshTimer = setTimeout(()=>wfRoutingRefreshOptions(false), 300);
    }catch(_e){}
  }
  function wfRoutingRefreshOptions(showToast){
    try{
      const stepList = document.getElementById('wfRouting_step_ids');
      const targetList = document.getElementById('wfRouting_target_ids');
      const ta = document.getElementById('wfDef_payload');
      if(!stepList || !targetList || !ta){ return; }
      const obj = JSON.parse(ta.value || '{}');
      const steps = Array.isArray(obj.steps)? obj.steps : [];
      const ids = steps.map(step => step && step.id ? String(step.id) : '').filter(Boolean);
      const options = ids.map(id => `<option value="${id}"></option>`).join('');
      stepList.innerHTML = options;
      targetList.innerHTML = options;
      if(showToast){ Toast.success('Step list refreshed'); }
    }catch(e){
      if(showToast){ Toast.error('Refresh failed: '+(e?.message||e)); }
    }
  }
  function wfRoutingClear(){
    ['wfRouting_step','wfRouting_success','wfRouting_failure'].forEach(id => {
      const el = document.getElementById(id);
      if(el){ el.value = ''; }
    });
  }
  function wfApplyRouting(){
    try{
      const ta = document.getElementById('wfDef_payload');
      if(!ta){ return; }
      const stepId = (document.getElementById('wfRouting_step')?.value || '').trim();
      if(!stepId){
        Toast.error('Select a step id to update.');
        return;
      }
      const success = (document.getElementById('wfRouting_success')?.value || '').trim();
      const failure = (document.getElementById('wfRouting_failure')?.value || '').trim();
      const obj = JSON.parse(ta.value || '{}');
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const step = obj.steps.find(s => String(s?.id || '') === stepId);
      if(!step){
        Toast.error('Step '+stepId+' not found.');
        return;
      }
      if(success){ step.on_success = success; } else { delete step.on_success; }
      if(failure){ step.on_failure = failure; } else { delete step.on_failure; }
      ta.value = JSON.stringify(obj, null, 2);
      wfRoutingRefreshOptions(false);
      Toast.success('Updated routing for '+stepId);
    }catch(e){
      Toast.error('Routing update failed: '+(e?.message||e));
    }
  }
  function wfInsertDelay(){
    try{
      const ta = document.getElementById('wfDef_payload');
      const obj = JSON.parse(ta.value||"{}");
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const id = 'delay_'+(obj.steps.length+1);
      obj.steps.push({ id, type: 'delay', config: { milliseconds: 1000 }});
      ta.value = JSON.stringify(obj,null,2);
      wfRoutingRefreshOptions(false);
      Toast.success('Inserted delay step');
    }catch(e){ Toast.error('Could not insert delay: '+(e?.message||e)); }
  }
  function wfInsertLog(){
    try{
      const ta = document.getElementById('wfDef_payload');
      const obj = JSON.parse(ta.value||"{}");
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const id = 'log_'+(obj.steps.length+1);
      obj.steps.push({ id, type: 'log', config: { message: 'Run {{ inputs.name || \"\" }}', level: 'info' }});
      ta.value = JSON.stringify(obj,null,2);
      wfRoutingRefreshOptions(false);
      Toast.success('Inserted log step');
    }catch(e){ Toast.error('Could not insert log: '+(e?.message||e)); }
  }
  function wfInsertBranch(){
    try{
      const ta = document.getElementById('wfDef_payload');
      const obj = JSON.parse(ta.value||"{}");
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const id = 'branch_'+(obj.steps.length+1);
      obj.steps.push({
        id,
        name: 'Decision '+id,
        type: 'branch',
        config: {
          condition: '{{ inputs.flag || last.text || "" }}',
          true_next: '',
          false_next: ''
        }
      });
      ta.value = JSON.stringify(obj,null,2);
      wfRoutingRefreshOptions(false);
      Toast.success('Inserted branch step');
    }catch(e){ Toast.error('Could not insert branch: '+(e?.message||e)); }
  }
  function wfInsertRSSFetch(){
    try{
      const ta = document.getElementById('wfDef_payload');
      const obj = JSON.parse(ta.value||"{}");
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const id = 'rss_'+(obj.steps.length+1);
      obj.steps.push({ id, type: 'rss_fetch', config: { urls: ['https://example.com/feed.xml'], limit: 5 }});
      ta.value = JSON.stringify(obj,null,2);
      wfRoutingRefreshOptions(false);
      Toast.success('Inserted rss_fetch step');
    }catch(e){ Toast.error('Could not insert rss_fetch: '+(e?.message||e)); }
  }
  function wfInsertEmbed(){
    try{
      const ta = document.getElementById('wfDef_payload');
      const obj = JSON.parse(ta.value||"{}");
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const id = 'embed_'+(obj.steps.length+1);
      obj.steps.push({ id, type: 'embed', config: { texts: '{{ last.text }}', collection: 'user_{{ user_id }}_workflows' }});
      ta.value = JSON.stringify(obj,null,2);
      wfRoutingRefreshOptions(false);
      Toast.success('Inserted embed step');
    }catch(e){ Toast.error('Could not insert embed: '+(e?.message||e)); }
  }
  function wfInsertTranslate(){
    try{
      const ta = document.getElementById('wfDef_payload');
      const obj = JSON.parse(ta.value||"{}");
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const id = 'translate_'+(obj.steps.length+1);
      obj.steps.push({ id, type: 'translate', config: { input: '{{ last.text }}', target_lang: 'en' }});
      ta.value = JSON.stringify(obj,null,2);
      wfRoutingRefreshOptions(false);
      Toast.success('Inserted translate step');
    }catch(e){ Toast.error('Could not insert translate: '+(e?.message||e)); }
  }
  function wfInsertSTT(){
    try{
      const ta = document.getElementById('wfDef_payload');
      const obj = JSON.parse(ta.value||"{}");
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const id = 'stt_'+(obj.steps.length+1);
      obj.steps.push({ id, type: 'stt_transcribe', config: { file_uri: 'file:///ABSOLUTE/PATH/TO/audio.wav', model: 'large-v3', word_timestamps: false }});
      ta.value = JSON.stringify(obj,null,2);
      wfRoutingRefreshOptions(false);
      Toast.success('Inserted stt_transcribe step');
    }catch(e){ Toast.error('Could not insert stt_transcribe: '+(e?.message||e)); }
  }
  function wfInsertNotify(){
    try{
      const ta = document.getElementById('wfDef_payload');
      const obj = JSON.parse(ta.value||"{}");
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const id = 'notify_'+(obj.steps.length+1);
      obj.steps.push({ id, type: 'notify', config: { url: 'https://hooks.slack.com/services/xxx', message: '{{ last.text }}' }});
      ta.value = JSON.stringify(obj,null,2);
      wfRoutingRefreshOptions(false);
      Toast.success('Inserted notify step');
    }catch(e){ Toast.error('Could not insert notify: '+(e?.message||e)); }
  }
  function wfInsertDiff(){
    try{
      const ta = document.getElementById('wfDef_payload');
      const obj = JSON.parse(ta.value||"{}");
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const id = 'diff_'+(obj.steps.length+1);
      obj.steps.push({ id, type: 'diff_change_detector', config: { current: '{{ last.text }}', method: 'ratio', threshold: 0.95 }});
      ta.value = JSON.stringify(obj,null,2);
      wfRoutingRefreshOptions(false);
      Toast.success('Inserted diff_change_detector step');
    }catch(e){ Toast.error('Could not insert diff change: '+(e?.message||e)); }
  }
  function wfInsertPrompt(){
    try{
      const ta = document.getElementById('wfDef_payload');
      const obj = JSON.parse(ta.value||"{}");
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const id = 'prompt_'+(obj.steps.length+1);
      obj.steps.push({ id, type: 'prompt', config: { template: 'Hello {{ inputs.name }}' }});
      ta.value = JSON.stringify(obj,null,2);
      wfRoutingRefreshOptions(false);
      Toast.success('Inserted prompt step');
    }catch(e){ Toast.error('Could not insert prompt: '+(e?.message||e)); }
  }
  function wfInsertProcessMedia(){
    try{
      const ta = document.getElementById('wfDef_payload');
      const obj = JSON.parse(ta.value||"{}");
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const id = 'proc_'+(obj.steps.length+1);
      obj.steps.push({ id, type: 'process_media', config: { kind: 'web_scraping', scrape_method: 'Individual URLs', url_input: 'https://example.com/article', summarize: true, temperature: 0.7 }});
      ta.value = JSON.stringify(obj,null,2);
      wfRoutingRefreshOptions(false);
      Toast.success('Inserted process_media step');
    }catch(e){ Toast.error('Could not insert process_media: '+(e?.message||e)); }
  }
  function wfInsertTTS(){
    try{
      const ta = document.getElementById('wfDef_payload');
      const obj = JSON.parse(ta.value||"{}");
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const id = 'tts_'+(obj.steps.length+1);
      obj.steps.push({ id, type: 'tts', config: { input: 'Narrate: {{ last.text || inputs.summary || "" }}', model: 'kokoro', voice: 'af_heart', response_format: 'mp3', speed: 1.0 }});
      ta.value = JSON.stringify(obj,null,2);
      wfRoutingRefreshOptions(false);
      Toast.success('Inserted TTS step');
    }catch(e){ Toast.error('Could not insert tts: '+(e?.message||e)); }
  }
  function wfInsertRagSearch(){
    try{
      const ta = document.getElementById('wfDef_payload');
      const obj = JSON.parse(ta.value||"{}");
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const id = 'rag_'+(obj.steps.length+1);
      obj.steps.push({ id, type: 'rag_search', config: { query: '{{ inputs.query || "example" }}', sources: ['media_db'], search_mode: 'hybrid', top_k: 5 }});
      ta.value = JSON.stringify(obj,null,2);
      wfRoutingRefreshOptions(false);
      Toast.success('Inserted RAG search step');
    }catch(e){ Toast.error('Could not insert rag_search: '+(e?.message||e)); }
  }
  function wfInsertMediaIngest(){
    try{
      const ta = document.getElementById('wfDef_payload');
      const obj = JSON.parse(ta.value||"{}");
      obj.steps = Array.isArray(obj.steps)? obj.steps : [];
      const id = 'ingest_'+(obj.steps.length+1);
      obj.steps.push({ id, type: 'media_ingest', config: { sources: [{ uri: 'file:///path/to/file.txt' }], extraction: { extract_text: true }, chunking: { method: 'simple', max_size: 800, overlap: 100 } }});
      ta.value = JSON.stringify(obj,null,2);
      wfRoutingRefreshOptions(false);
      Toast.success('Inserted media_ingest step');
    }catch(e){ Toast.error('Could not insert media_ingest: '+(e?.message||e)); }
  }
  function wfCopyJson(id){
    try{
      const txt = document.getElementById(id).textContent||'';
      navigator.clipboard.writeText(txt).then(()=>Toast.success('Copied')).catch(()=>Toast.error('Copy failed'));
    }catch(e){ Toast.error('Copy failed: '+(e?.message||e)); }
  }
  function wfClearJson(id){ try{ document.getElementById(id).textContent=''; }catch(e){} }
  </script>
</div>

<div id="tabWorkflowsRuns" class="tab-content">
  <h2>Workflows: Runs</h2>
  <div class="form-group">
    <label>Quick</label>
    <select id="wfList_quick">
      <option value="">(none)</option>
      <option value="1">last 1h</option>
      <option value="24">last 24h</option>
      <option value="168">last 7d</option>
      <option value="720">last 30d</option>
    </select>
    <div id="wfList_status_chips" style="margin-bottom:8px">
      <span class="chip" data-status="queued" role="button" tabindex="0" style="display:inline-block;padding:4px 8px;border:1px solid var(--color-base-40);border-radius:12px;margin-right:6px;cursor:pointer">queued</span>
      <span class="chip" data-status="running" role="button" tabindex="0" style="display:inline-block;padding:4px 8px;border:1px solid var(--color-base-40);border-radius:12px;margin-right:6px;cursor:pointer">running</span>
      <span class="chip" data-status="paused" role="button" tabindex="0" style="display:inline-block;padding:4px 8px;border:1px solid var(--color-base-40);border-radius:12px;margin-right:6px;cursor:pointer">paused</span>
      <span class="chip" data-status="waiting_human" role="button" tabindex="0" style="display:inline-block;padding:4px 8px;border:1px solid var(--color-base-40);border-radius:12px;margin-right:6px;cursor:pointer">waiting_human</span>
      <span class="chip" data-status="waiting_approval" role="button" tabindex="0" style="display:inline-block;padding:4px 8px;border:1px solid var(--color-base-40);border-radius:12px;margin-right:6px;cursor:pointer">waiting_approval</span>
      <span class="chip" data-status="succeeded" role="button" tabindex="0" style="display:inline-block;padding:4px 8px;border:1px solid var(--color-base-40);border-radius:12px;margin-right:6px;cursor:pointer">succeeded</span>
      <span class="chip" data-status="failed" role="button" tabindex="0" style="display:inline-block;padding:4px 8px;border:1px solid var(--color-base-40);border-radius:12px;margin-right:6px;cursor:pointer">failed</span>
      <span class="chip" data-status="cancelled" role="button" tabindex="0" style="display:inline-block;padding:4px 8px;border:1px solid var(--color-base-40);border-radius:12px;margin-right:6px;cursor:pointer">cancelled</span>
    </div>
    <label>Last N Hours</label>
    <input type="number" id="wfList_last_hours" min="1" style="width:80px" placeholder="e.g., 24">
    <label>Status (comma)</label>
    <input type="text" id="wfList_status" placeholder="succeeded,failed">
    <label>Workflow ID</label>
    <input type="text" id="wfList_workflow_id" placeholder="optional">
    <label>Created After</label>
    <input type="datetime-local" id="wfList_created_after" placeholder="YYYY-MM-DD HH:MM">
    <label>Created Before</label>
    <input type="datetime-local" id="wfList_created_before" placeholder="YYYY-MM-DD HH:MM">
    <label>Date Presets</label>
    <select id="wfList_presets">
      <option value="">(none)</option>
      <option value="today">Today</option>
      <option value="yesterday">Yesterday</option>
      <option value="lasthour">Last hour</option>
      <option value="last24h">Last 24h</option>
      <option value="thisweek">This week</option>
      <option value="last7d">Last 7 days</option>
      <option value="last30d">Last 30 days</option>
    </select>
    <label>Order By</label>
    <select id="wfList_order_by">
      <option value="created_at" selected>created_at</option>
      <option value="started_at">started_at</option>
      <option value="ended_at">ended_at</option>
    </select>
    <label>Order</label>
    <select id="wfList_order">
      <option value="desc" selected>desc</option>
      <option value="asc">asc</option>
    </select>
    <label>Limit</label>
    <input type="number" id="wfList_limit" value="25" min="1" max="200" style="width:80px">
    <label>Offset</label>
    <input type="number" id="wfList_offset" value="0" min="0" style="width:80px">
    <label style="margin-left:8px" title="When enabled, the cursor token is used and the 'offset' value is ignored."><input type="checkbox" id="wfList_use_cursor"> Use cursor</label>
    <button class="api-button btn-sm" id="wfCursor_help" title="What is cursor pagination?">Cursor Help</button>
    <label>Cursor</label>
    <input type="text" id="wfList_cursor" placeholder="opaque token" style="width:260px">
    <button class="api-button btn-sm" id="wfCursor_reset">Reset cursor</button>
      <button class="api-button" id="wfList_runs">List Runs</button>
      <button class="api-button" id="wfList_copy_curl" title="Copy the current runs list request as cURL, including cursor if enabled">Copy cURL (List)</button>
      <button class="api-button" id="wfList_copy_link" title="Copy a URL with these filters applied">Copy Share Link</button>
      <button class="api-button" id="wfList_clear_filters">Clear Filters</button>
  </div>
  <div class="form-group">
    <label>Run ID</label>
    <input type="text" id="wfRun_run_id" placeholder="uuid" />
    <button class="api-button" id="wfRun_get">Get Status</button>
    <button class="api-button" id="wfRun_get_events">Get Events</button>
    <label style="display:inline-flex;align-items:center;gap:6px;margin-left:6px">
      <input type="checkbox" id="wfWatch_toggle" aria-label="Toggle run watcher">
      Watching
      <span id="wfWatch_spinner" class="wf-inline-spinner" style="display:none" aria-hidden="true"></span>
    </label>
    <label>Events Limit</label>
    <input type="number" id="wfEvents_limit" min="1" max="1000" value="500" style="width:90px"/>
    <button class="api-button" id="wfEvents_next">Load Next Events</button>
    <label style="margin-left:8px"><input type="checkbox" id="wfEvents_auto"> Auto-refresh</label>
    <label style="margin-left:8px"><input type="checkbox" id="wfEvents_tail"> Auto-follow tail</label>
    <label style="margin-left:8px" title="Use server-provided Next-Cursor header for paging instead of since=last_seq"><input type="checkbox" id="wfEvents_use_cursor"> Use cursor (events)</label>
    <button class="api-button btn-sm" id="wfEvents_next_cursor_btn" disabled>Next (cursor)</button>
    <label style="margin-left:8px">Filter (types)</label>
    <input type="text" id="wfEvents_filter" placeholder="step_failed,run_failed" style="width:220px"/>
    <button class="api-button btn-sm" id="wfEvents_error_filter" title="Filter to failure and warning events">Errors/Warns</button>
    <button class="api-button" id="wfArtifacts_list">List Artifacts</button>
    <button class="api-button" id="wfArtifacts_dl_server">Download All (server)</button>
    <button class="api-button" id="wfArtifacts_dl_client">Download All (client)</button>
  </div>
  <div id="wfEvents_type_chips" class="wf-type-chips"></div>
  <div class="form-group">
    <button class="api-button" id="wfRun_pause">Pause</button>
    <button class="api-button" id="wfRun_resume">Resume</button>
    <button class="api-button" id="wfRun_cancel">Cancel</button>
    <button class="api-button" id="wfRun_retry">Retry</button>
  </div>
  <div class="form-group" style="margin-bottom:4px">
    <button class="api-button" id="wfRun_copy_json">Copy</button>
    <button class="api-button" id="wfRun_clear_json">Clear</button>
  </div>
  <pre id="wfRun_result" class="json-viewer"></pre>
  <div id="wfRuns_cursor_hint" style="margin-top:4px;color:var(--color-base-0)"></div>
  <div class="form-group" style="margin-top:4px;color:var(--color-base-0)">
    <span id="wfEvents_info"></span>
  </div>
  <div class="form-group" style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:6px;color:var(--color-base-0)">
    <label style="margin:0">Events next cursor</label>
    <input type="text" id="wfEvents_next_cursor_value" readonly style="min-width:280px" placeholder="(available when server returns Next-Cursor)">
    <button class="api-button btn-sm" id="wfEvents_copy_cursor" disabled>Copy</button>
  </div>
  <div id="wfEvents_table"></div>
  <div id="wfRun_list" class="json-viewer" style="margin-top:8px"></div>
  <div class="form-group">
    <button class="api-button" id="wfList_prev" disabled>Prev</button>
    <button class="api-button" id="wfList_next" disabled>Next</button>
    <button class="api-button" id="wfList_next_cursor" disabled>Next (cursor)</button>
    <span id="wfList_page_info" style="margin-left:8px;color:var(--color-base-0)"></span>
    <span id="wfList_cursor_info" style="margin-left:8px;color:var(--color-base-0)"></span>
    <div style="margin-top:6px; display:flex; gap:6px; align-items:center; flex-wrap:wrap">
      <label style="margin:0">next_cursor</label>
      <input type="text" id="wfList_next_cursor_value" style="min-width:420px" readonly placeholder="(will appear after listing)"/>
      <button class="api-button" id="wfList_copy_cursor_btn" disabled>Copy</button>
    </div>
  </div>
  <div id="wfRun_artifacts" class="json-viewer"></div>
  <div class="card" style="margin-top:16px">
    <h3>Webhook DLQ (Admin)</h3>
    <div class="form-group">
      <label>Limit</label>
      <input type="number" id="wfDlq_limit" value="50" min="1" max="500" style="width:80px">
      <label>Offset</label>
      <input type="number" id="wfDlq_offset" value="0" min="0" style="width:80px">
      <button class="api-button" id="wfDlq_refresh">Refresh</button>
    </div>
    <div id="wfDlq_table"></div>
  </div>
  <script>
  function wfQuickHours(){
    const v = document.getElementById('wfList_quick').value;
    document.getElementById('wfList_last_hours').value = v || '';
  }
  function wfCopyRunsCurl(){
    try{
      const params = new URLSearchParams();
      // statuses
      const picked = wfSelectedStatuses();
      const statusText = (document.getElementById('wfList_status').value||'').trim();
      if (picked.length){ picked.forEach(s=>params.append('status', s)); }
      if (statusText){ statusText.split(',').map(s=>s.trim()).filter(Boolean).forEach(s=>params.append('status', s)); }
      // basic fields
      const setIf = (k,id)=>{ const v=(document.getElementById(id)?.value||'').trim(); if (v) params.set(k, v); };
      setIf('workflow_id','wfList_workflow_id');
      const caRaw = (document.getElementById('wfList_created_after')?.value||'').trim(); if (caRaw) params.set('created_after', new Date(caRaw).toISOString());
      const cbRaw = (document.getElementById('wfList_created_before')?.value||'').trim(); if (cbRaw) params.set('created_before', new Date(cbRaw).toISOString());
      setIf('order_by','wfList_order_by'); setIf('order','wfList_order'); setIf('limit','wfList_limit');
      const useCursor = !!document.getElementById('wfList_use_cursor')?.checked;
      const cursorVal = (document.getElementById('wfList_cursor')?.value||'').trim();
      if (useCursor && cursorVal){ params.set('cursor', cursorVal); }
      else { setIf('offset','wfList_offset'); }
      const qs = params.toString();
      const base = (apiClient && apiClient.baseUrl) ? apiClient.baseUrl : 'http://localhost:8000';
      const token = (apiClient && apiClient.token) ? apiClient.token : '$API_KEY';
      const url = `${base}/api/v1/workflows/runs${qs ? ('?'+qs): ''}`;
      const snippet = `curl -sS -H "X-API-KEY: ${token}" "${url}"`;
      navigator.clipboard.writeText(snippet).then(()=>Toast.success('Copied')).catch(()=>Toast.error('Copy failed'));
    }catch(e){ Toast.error('Copy failed: '+(e?.message||e)); }
  }
  function wfCopyShareLink(){
    try{
      const state = wfCaptureFilters();
      const url = new URL(window.location.href);
      const params = url.searchParams;
      Array.from(params.keys()).forEach(key => { if (key.startsWith('wf_')) params.delete(key); });
      if (state.last_hours) params.set('wf_last_hours', state.last_hours);
      if (state.status) params.set('wf_status_text', state.status);
      if (Array.isArray(state.selected_statuses)){
        state.selected_statuses.filter(Boolean).forEach(st => params.append('wf_status', st));
      }
      if (state.workflow_id) params.set('wf_workflow_id', state.workflow_id);
      if (state.created_after) params.set('wf_created_after', state.created_after);
      if (state.created_before) params.set('wf_created_before', state.created_before);
      if (state.order_by) params.set('wf_order_by', state.order_by);
      if (state.order) params.set('wf_order', state.order);
      if (state.limit) params.set('wf_limit', state.limit);
      if (state.use_cursor){
        params.set('wf_use_cursor', '1');
        params.delete('wf_offset');
        if (state.cursor) params.set('wf_cursor', state.cursor);
        else params.delete('wf_cursor');
      } else {
        params.set('wf_use_cursor', '0');
        params.delete('wf_cursor');
        if (state.offset) params.set('wf_offset', state.offset);
      }
      if (state.preset) params.set('wf_preset', state.preset);
      else params.delete('wf_preset');
      const serialized = params.toString();
      const shareUrl = `${url.origin}${url.pathname}${serialized ? `?${serialized}` : ''}${url.hash}`;
      window.history.replaceState({}, '', `${url.pathname}${serialized ? `?${serialized}` : ''}${url.hash}`);
      navigator.clipboard.writeText(shareUrl).then(()=>Toast.success('Share link copied')).catch(()=>Toast.error('Copy failed'));
    }catch(e){
      Toast.error('Could not build link: '+(e?.message||e));
    }
  }
  async function wfDlqLoad(){
    try{
      const limit = parseInt(document.getElementById('wfDlq_limit').value||'50');
      const offset = parseInt(document.getElementById('wfDlq_offset').value||'0');
      const resp = await apiClient.makeRequest('GET', `/api/v1/workflows/webhooks/dlq?limit=${limit}&offset=${offset}`);
      const items = resp?.items || [];
      let html = '<table style="width:100%;border-collapse:collapse">';
      html += '<thead><tr><th style="text-align:left;padding:4px;border-bottom:1px solid var(--color-base-40)">ID</th><th style="text-align:left;padding:4px;border-bottom:1px solid var(--color-base-40)">Tenant</th><th style="text-align:left;padding:4px;border-bottom:1px solid var(--color-base-40)">Run</th><th style="text-align:left;padding:4px;border-bottom:1px solid var(--color-base-40)">URL</th><th style="text-align:left;padding:4px;border-bottom:1px solid var(--color-base-40)">Attempts</th><th style="text-align:left;padding:4px;border-bottom:1px solid var(--color-base-40)">Next Attempt</th><th style="text-align:left;padding:4px;border-bottom:1px solid var(--color-base-40)">Last Error</th><th style="text-align:left;padding:4px;border-bottom:1px solid var(--color-base-40)">Actions</th></tr></thead>';
      html += '<tbody>';
      items.forEach(it => {
        const id = it.id; const t = it.tenant_id||''; const rid = it.run_id||''; const url = it.url||''; const at = it.attempts||0; const next = it.next_attempt_at||''; const err = it.last_error||'';
        html += `<tr>
          <td style="padding:4px;border-bottom:1px solid var(--color-base-30)">${id}</td>
          <td style="padding:4px;border-bottom:1px solid var(--color-base-30)">${t}</td>
          <td style="padding:4px;border-bottom:1px solid var(--color-base-30)">${rid}</td>
          <td style="padding:4px;border-bottom:1px solid var(--color-base-30)"><code>${escapeHtml(url)}</code></td>
          <td style="padding:4px;border-bottom:1px solid var(--color-base-30)">${at}</td>
          <td style="padding:4px;border-bottom:1px solid var(--color-base-30)">${next||''}</td>
          <td style="padding:4px;border-bottom:1px solid var(--color-base-30)"><code>${escapeHtml(String(err)).slice(0,120)}</code></td>
          <td style="padding:4px;border-bottom:1px solid var(--color-base-30)"><button class=\"api-button\" onclick=\"wfDlqReplay(${id})\">Replay</button></td>
        </tr>`;
      });
      html += '</tbody></table>';
      document.getElementById('wfDlq_table').innerHTML = html;
    }catch(e){ document.getElementById('wfDlq_table').innerHTML = '<em>Failed to load DLQ</em>'; Toast.error('DLQ load failed: '+(e?.message||e)); }
  }
  async function wfDlqReplay(id){
    try{
      const r = await apiClient.makeRequest('POST', `/api/v1/workflows/webhooks/dlq/${id}/replay`);
      if (r && r.ok) { Toast.success('Replay dispatched'); wfDlqLoad(); }
      else { Toast.error('Replay failed'); }
    }catch(e){ Toast.error('Replay error: '+(e?.message||e)); }
  }
  const WF_FILTERS_KEY_PREFIX = 'workflows-runs-filters';
  let wfFiltersStorageKey = `${WF_FILTERS_KEY_PREFIX}::anon`;
  let wfFiltersKeyPromise = null;
  async function wfEnsureFiltersKey(){
    if (wfFiltersKeyPromise){
      try{ return await wfFiltersKeyPromise; }catch(_){ /* fallthrough */ }
    }
    wfFiltersKeyPromise = (async ()=>{
      let context = 'anon';
      try{
        if (window.apiClient && typeof apiClient.makeRequest === 'function'){
          const me = await apiClient.makeRequest('GET', '/api/v1/auth/me');
          if (me && typeof me.id !== 'undefined'){
            const tenant = me.tenant_id || 'default';
            context = `${tenant}:${me.id}`;
          } else if (apiClient.authMode === 'single-user'){
            context = 'single';
          }
        } else if (window.apiClient && apiClient.authMode === 'single-user'){
          context = 'single';
        }
      }catch(e){
        if (window.apiClient && apiClient.authMode === 'single-user'){
          context = 'single';
        }
      }
      const newKey = `${WF_FILTERS_KEY_PREFIX}::${context}`;
      if (newKey !== wfFiltersStorageKey){
        let snapshot = null;
        if (typeof Utils !== 'undefined'){
          snapshot = Utils.getFromStorage(newKey);
          if (!snapshot){
            snapshot = Utils.getFromStorage(wfFiltersStorageKey) || Utils.getFromStorage(WF_FILTERS_KEY_PREFIX) || Utils.getFromStorage(`${WF_FILTERS_KEY_PREFIX}::anon`);
          }
        }
        wfFiltersStorageKey = newKey;
        if (snapshot && typeof Utils !== 'undefined'){
          Utils.saveToStorage(newKey, snapshot);
        }
      }
      return wfFiltersStorageKey;
    })();
    try{
      return await wfFiltersKeyPromise;
    }catch(e){
      wfFiltersKeyPromise = null;
      return wfFiltersStorageKey;
    }
  }
  // Persist/restore runs filters
  function wfCaptureFilters(){
    return {
      last_hours: (document.getElementById('wfList_last_hours')?.value||'').trim(),
      status: (document.getElementById('wfList_status')?.value||'').trim(),
      selected_statuses: wfSelectedStatuses(),
      workflow_id: (document.getElementById('wfList_workflow_id')?.value||'').trim(),
      created_after: (document.getElementById('wfList_created_after')?.value||'').trim(),
      created_before: (document.getElementById('wfList_created_before')?.value||'').trim(),
      order_by: (document.getElementById('wfList_order_by')?.value||'').trim(),
      order: (document.getElementById('wfList_order')?.value||'').trim(),
      limit: (document.getElementById('wfList_limit')?.value||'').trim(),
      offset: (document.getElementById('wfList_offset')?.value||'').trim(),
      use_cursor: !!(document.getElementById('wfList_use_cursor')?.checked),
      cursor: (document.getElementById('wfList_cursor')?.value||'').trim(),
      preset: (document.getElementById('wfList_presets')?.value||'').trim(),
    };
  }
  function wfSaveFilters(){
    try{
      const payload = wfCaptureFilters();
      if (typeof Utils !== 'undefined'){
        const keyNow = wfFiltersStorageKey;
        Utils.saveToStorage(keyNow, payload);
        wfEnsureFiltersKey().then(key=>{
          try{ Utils.saveToStorage(key, payload); }catch(_){ }
        }).catch(()=>{});
      }
    }catch(_){ }
  }
  async function wfRestoreFilters(){
    try{
      const key = await wfEnsureFiltersKey();
      let saved = null;
      if (typeof Utils !== 'undefined'){
        const candidates = [key, WF_FILTERS_KEY_PREFIX, `${WF_FILTERS_KEY_PREFIX}::anon`];
        for (const k of candidates){
          if (!k) continue;
          const value = Utils.getFromStorage(k);
          if (value){
            saved = value;
            break;
          }
        }
      }
      if (!saved) return;
      const setv = (id,v)=>{ try{ const el=document.getElementById(id); if (el && v!=null) el.value = v; }catch(_){ } };
      setv('wfList_last_hours', saved.last_hours);
      setv('wfList_status', saved.status);
      setv('wfList_workflow_id', saved.workflow_id);
      setv('wfList_created_after', saved.created_after);
      setv('wfList_created_before', saved.created_before);
      setv('wfList_order_by', saved.order_by);
      setv('wfList_order', saved.order);
      setv('wfList_limit', saved.limit);
      setv('wfList_offset', saved.offset);
      setv('wfList_presets', saved.preset);
      try{ const uc=document.getElementById('wfList_use_cursor'); if (uc) uc.checked = !!saved.use_cursor; }catch(_){ }
      setv('wfList_cursor', saved.cursor);
      if (Array.isArray(saved.selected_statuses)){
        const pick = new Set(saved.selected_statuses.map(s=>String(s||'').toLowerCase()));
        document.querySelectorAll('#wfList_status_chips .chip').forEach(ch=>{
          const st = (ch.getAttribute('data-status')||'').toLowerCase();
          if (pick.has(st)){
            ch.setAttribute('data-selected', '1');
            ch.style.background = 'var(--color-base-80)';
          } else {
            ch.setAttribute('data-selected', '0');
            ch.style.background = '';
          }
        });
      }
      wfApplyQueryFilters();
    }catch(_){ }
  }
  function wfApplyQueryFilters(){
    try{
      const params = new URLSearchParams(window.location.search);
      const hasShare = Array.from(params.keys()).some(k=>k.startsWith('wf_'));
      if (!hasShare) return false;
      let mutated = false;
      const assign = (id, value)=>{
        if (value == null || value === '') return;
        const el = document.getElementById(id);
        if (el){
          el.value = value;
          mutated = true;
        }
      };
      assign('wfList_last_hours', params.get('wf_last_hours'));
      assign('wfList_status', params.get('wf_status_text'));
      const statuses = params.getAll('wf_status');
      if (statuses && statuses.length){
        const set = new Set(statuses.map(s=>String(s||'').toLowerCase()));
        document.querySelectorAll('#wfList_status_chips .chip').forEach(ch=>{
          const st = (ch.getAttribute('data-status')||'').toLowerCase();
          if (set.has(st)){
            ch.setAttribute('data-selected','1');
            ch.style.background = 'var(--color-base-80)';
          } else {
            ch.setAttribute('data-selected','0');
            ch.style.background = '';
          }
        });
        mutated = true;
      }
      assign('wfList_workflow_id', params.get('wf_workflow_id'));
      assign('wfList_created_after', params.get('wf_created_after'));
      assign('wfList_created_before', params.get('wf_created_before'));
      assign('wfList_order_by', params.get('wf_order_by'));
      assign('wfList_order', params.get('wf_order'));
      assign('wfList_limit', params.get('wf_limit'));
      const offsetVal = params.get('wf_offset');
      if (offsetVal != null){
        const el = document.getElementById('wfList_offset');
        if (el){
          el.value = offsetVal;
          mutated = true;
        }
      }
      const presetVal = params.get('wf_preset');
      if (presetVal){
        const presetEl = document.getElementById('wfList_presets');
        if (presetEl){
          presetEl.value = presetVal;
          mutated = true;
          if (!params.has('wf_created_after') && !params.has('wf_created_before')){
            try{ wfApplyPreset(); }catch(_){ }
          }
        }
      }
      const useCursorFlag = params.get('wf_use_cursor');
      const useCursorEl = document.getElementById('wfList_use_cursor');
      if (useCursorEl){
        const shouldUse = useCursorFlag === '1' || useCursorFlag === 'true';
        const explicitFalse = useCursorFlag === '0' || useCursorFlag === 'false';
        if (useCursorFlag !== null){
          useCursorEl.checked = shouldUse;
          mutated = mutated || shouldUse || explicitFalse;
        }
      }
      if (useCursorEl && useCursorEl.checked){
        const cursorVal = params.get('wf_cursor');
        if (cursorVal != null){
          const cursorEl = document.getElementById('wfList_cursor');
          if (cursorEl){
            cursorEl.value = cursorVal;
            mutated = true;
          }
        }
      } else if (useCursorFlag !== null){
        const cursorEl = document.getElementById('wfList_cursor');
        if (cursorEl){
          cursorEl.value = '';
        }
      }
      if (mutated){
        wfSaveFilters();
      }
      return mutated;
    }catch(_){
      return false;
    }
  }
  function wfToggleChip(el){
    const selected = el.getAttribute('data-selected') === '1';
    if (selected){ el.setAttribute('data-selected','0'); el.style.background=''; }
    else { el.setAttribute('data-selected','1'); el.style.background='var(--color-base-80)'; }
  }
  function wfSelectedStatuses(){
    const chips = document.querySelectorAll('#wfList_status_chips .chip[data-selected="1"]');
    return Array.from(chips).map(c=>c.getAttribute('data-status'));
  }
  let wfLastList = { limit: 0, offset: 0, next_offset: null, next_cursor: null };
  async function wfListRuns(){
    try{
      wfSaveFilters();
      const params = new URLSearchParams();
      const picked = wfSelectedStatuses();
      const statusText = (document.getElementById('wfList_status').value||'').trim();
      if (picked.length){ picked.forEach(s=>params.append('status', s)); }
      if (statusText){ statusText.split(',').map(s=>s.trim()).filter(Boolean).forEach(s=>params.append('status', s)); }
      const wf = document.getElementById('wfList_workflow_id').value.trim(); if (wf){ params.set('workflow_id', wf); }
      const caRaw = document.getElementById('wfList_created_after').value.trim();
      const cbRaw = document.getElementById('wfList_created_before').value.trim();
      const ca = caRaw ? new Date(caRaw).toISOString() : '';
      const cb = cbRaw ? new Date(cbRaw).toISOString() : '';
      if (ca){ params.set('created_after', ca); }
      if (cb){ params.set('created_before', cb); }
      const lh = document.getElementById('wfList_last_hours').value.trim(); if (lh){ params.set('last_n_hours', lh); }
      params.set('order_by', document.getElementById('wfList_order_by').value);
      params.set('order', document.getElementById('wfList_order').value);
      params.set('limit', document.getElementById('wfList_limit').value);
      const useCursor = !!document.getElementById('wfList_use_cursor').checked;
      const cursorVal = (document.getElementById('wfList_cursor').value||'').trim();
      if (useCursor && cursorVal){ params.set('cursor', cursorVal); }
      else { params.set('offset', document.getElementById('wfList_offset').value); }
      const url = '/api/v1/workflows/runs?' + params.toString();
      const resp = await apiClient.makeRequest('GET', url);
      set('wfRun_result', resp);
      renderRunTable(resp);
      // Update pager state
      const limit = parseInt(document.getElementById('wfList_limit').value || '25');
      const offset = parseInt(document.getElementById('wfList_offset').value || '0');
      wfLastList = { limit, offset, next_offset: resp?.next_offset ?? null, next_cursor: resp?.next_cursor ?? null };
      document.getElementById('wfList_prev').disabled = useCursor ? true : (offset <= 0);
      document.getElementById('wfList_next').disabled = useCursor ? true : (resp?.next_offset == null);
      document.getElementById('wfList_next_cursor').disabled = useCursor ? (resp?.next_cursor == null) : true;
      const count = Array.isArray(resp?.runs) ? resp.runs.length : 0;
      document.getElementById('wfList_page_info').textContent = `showing ${count} (limit ${limit}) offset ${offset}`;
      const nc = resp?.next_cursor || '';
      const infoEl = document.getElementById('wfList_cursor_info');
      if (infoEl){
        if (useCursor){
          if (nc){ infoEl.innerHTML = `next_cursor: ${String(nc).slice(0,24)}… <button class=\"api-button btn-sm\" onclick=\"wfCopyNextCursor()\">Copy</button>`; }
          else { infoEl.innerHTML = 'next_cursor: (none)'; }
        } else {
          infoEl.innerHTML = '';
        }
      }
      const ncInput = document.getElementById('wfList_next_cursor_value');
      const ncBtn = document.getElementById('wfList_copy_cursor_btn');
      if (ncInput){ ncInput.value = nc || ''; }
      if (ncBtn){ ncBtn.disabled = !nc; }
      try{
        const hint = document.getElementById('wfRuns_cursor_hint');
        if (hint){
          if (useCursor && nc){
            hint.innerHTML = `Tip: A next_cursor is available below. You can copy it here: <button class=\"api-button btn-sm\" onclick=\"wfCopyNextCursor()\">Copy next_cursor</button>`;
          } else {
            hint.innerHTML = '';
          }
        }
      }catch(_){ }
    }catch(e){ Toast.error('List runs failed: '+(e?.message||e)); }
  }
  function wfPrevPage(){ try{ const off = Math.max(0, (wfLastList.offset||0) - (wfLastList.limit||25)); document.getElementById('wfList_offset').value = off; wfListRuns(); }catch(e){} }
  function wfNextPage(){ try{ if (wfLastList.next_offset != null){ document.getElementById('wfList_offset').value = wfLastList.next_offset; wfListRuns(); } }catch(e){} }
  function wfNextCursor(){ try{ if (wfLastList.next_cursor){ const inp=document.getElementById('wfList_cursor'); inp.value = wfLastList.next_cursor; wfListRuns(); } }catch(e){} }
  function wfResetCursor(){ try{ const inp=document.getElementById('wfList_cursor'); if (inp){ inp.value=''; wfSaveFilters(); } Toast.info('Cursor cleared'); }catch(_){ }
  function wfCopyNextCursor(){ try{ const v=(document.getElementById('wfList_next_cursor_value')?.value||''); if (!v){ Toast.error('No next_cursor'); return; } navigator.clipboard.writeText(v).then(()=>Toast.success('Copied')).catch(()=>Toast.error('Copy failed')); }catch(e){ Toast.error('Copy failed'); } }
  function wfShowCursorHelp(){
    try{
      const text = [
        'Cursor pagination returns an opaque token (next_cursor) representing your current position in a stable order. ',
        'Use this token as the "cursor" parameter on subsequent requests to fetch the next page. ',
        'When a cursor value is provided, the numeric "offset" is ignored. ',
        'This approach avoids missed or duplicated items when new records arrive or timestamps tie.',
        '',
        'Tips:',
        ' - Toggle "Use cursor" to switch from offset paging to cursor paging.',
        ' - Copy the full next_cursor from the field below the pager or use the inline Copy buttons.',
        ' - The token includes ordering info; do not edit it.'
      ].join('\n');
      wfModalShow({
        title: 'Cursor pagination help',
        bodyText: text,
        copyText: text,
        wrapPre: true,
        focusSelector: '#wfModal_copy_btn'
      });
    }catch(_){ }
  }
  async function wfGetRun(){ try{ const id=val('wfRun_run_id'); const r=await apiClient.makeRequest('GET',`/api/v1/workflows/runs/${id}`); set('wfRun_result',r);}catch(e){Toast.error(e?.message||e);} }
  let wfEventsState = { runId: null, lastSeq: null, nextCursor: null, timer: null, followTail: false, types: new Set() };
  async function wfFetchEvents(urlRelative){
    try{
      const base = (apiClient && apiClient.baseUrl) ? apiClient.baseUrl : '';
      const url = base + urlRelative;
      const headers = { 'Accept': 'application/json' };
      if (apiClient && apiClient.token){
        if (apiClient.authMode === 'multi-user' && !apiClient.preferApiKeyInMultiUser){
          headers['Authorization'] = `Bearer ${apiClient.token}`;
        } else {
          headers['X-API-KEY'] = apiClient.token;
        }
      }
      const t0 = performance.now();
      const resp = await fetch(url, { method:'GET', headers });
      const latency = Math.round(performance.now()-t0);
      if (!resp.ok){ throw new Error(`HTTP ${resp.status}`); }
      const data = await resp.json();
      const nextCursor = resp.headers.get('Next-Cursor');
      return { data, latency, nextCursor };
    }catch(e){ throw e; }
  }
  let wfPayloadCache = {};
  async function wfGetEvents(){
    try{
      const id=val('wfRun_run_id');
      const limit = parseInt(document.getElementById('wfEvents_limit').value||'500');
      const typesRaw = (document.getElementById('wfEvents_filter')?.value||'').trim();
      const params = new URLSearchParams({ limit: String(limit) });
      if (typesRaw){ typesRaw.split(',').map(s=>s.trim()).filter(Boolean).forEach(t=>params.append('types', t)); }
      const { data: r, latency, nextCursor } = await wfFetchEvents(`/api/v1/workflows/runs/${id}/events?${params.toString()}`);
      set('wfRun_result', r);
      wfEventsState.runId = id;
      wfEventsState.lastSeq = Array.isArray(r)&&r.length? r[r.length-1].event_seq : null;
      wfEventsState.nextCursor = nextCursor || null;
      const btn = document.getElementById('wfEvents_next_cursor_btn'); if (btn){ btn.disabled = !(document.getElementById('wfEvents_use_cursor')?.checked && wfEventsState.nextCursor); }
      wfRenderEventsTable(r, latency, false);
    }catch(e){Toast.error(e?.message||e);} }
  async function wfGetNextEvents(){
    try{
      const id = val('wfRun_run_id');
      const limit = parseInt(document.getElementById('wfEvents_limit').value||'500');
      const typesRaw = (document.getElementById('wfEvents_filter')?.value||'').trim();
      const params = new URLSearchParams({ limit: String(limit) });
      const useCursor = !!document.getElementById('wfEvents_use_cursor')?.checked;
      if (useCursor && wfEventsState.nextCursor){ params.set('cursor', wfEventsState.nextCursor); }
      else if (wfEventsState.runId===id && wfEventsState.lastSeq!=null){ params.set('since', String(wfEventsState.lastSeq)); }
      if (typesRaw){ typesRaw.split(',').map(s=>s.trim()).filter(Boolean).forEach(t=>params.append('types', t)); }
      let url = `/api/v1/workflows/runs/${id}/events?${params.toString()}`;
      const { data: r, latency, nextCursor } = await wfFetchEvents(url);
      // Append to existing result view
      const cur = (()=>{ try{ return JSON.parse(document.getElementById('wfRun_result').textContent||'[]'); }catch(e){ return []; } })();
      const merged = Array.isArray(cur)? cur.concat(r) : r;
      set('wfRun_result', merged);
      if (Array.isArray(r)&&r.length){ wfEventsState.lastSeq = r[r.length-1].event_seq; wfEventsState.runId = id; }
      wfEventsState.nextCursor = nextCursor || wfEventsState.nextCursor;
      const btn2 = document.getElementById('wfEvents_next_cursor_btn'); if (btn2){ btn2.disabled = !(useCursor && wfEventsState.nextCursor); }
      wfRenderEventsTable(r, latency, true);
    }catch(e){ Toast.error(e?.message||e); }
  }
  function wfToggleAutoEvents(){
    try{
      const chk = document.getElementById('wfEvents_auto');
      if (chk.checked){
        if (wfEventsState.timer) clearInterval(wfEventsState.timer);
        wfEventsState.timer = setInterval(()=>{ try{ wfGetNextEvents(); }catch(e){} }, 1000);
        // Ensure interval is cleared when tab/page is hidden or unloaded
        const _wfCleanup = ()=>{ try{ if (wfEventsState.timer){ clearInterval(wfEventsState.timer); wfEventsState.timer = null; } }catch(e){} };
        document.addEventListener('visibilitychange', ()=>{ if (document.hidden) _wfCleanup(); }, { once: true });
        window.addEventListener('beforeunload', _wfCleanup, { once: true });
      } else {
        if (wfEventsState.timer) { clearInterval(wfEventsState.timer); wfEventsState.timer = null; }
      }
    }catch(e){}
  }
  function wfClearFilters(){
    try{
      // Clear run list filters
      document.getElementById('wfList_status').value = '';
      document.getElementById('wfList_workflow_id').value = '';
      document.getElementById('wfList_last_hours').value = '';
      document.getElementById('wfList_created_after').value = '';
      document.getElementById('wfList_created_before').value = '';
      document.getElementById('wfList_presets').value = '';
      document.getElementById('wfList_order_by').value = 'created_at';
      document.getElementById('wfList_order').value = 'desc';
      const limitEl = document.getElementById('wfList_limit'); if (limitEl) limitEl.value = '25';
      const offsetEl = document.getElementById('wfList_offset'); if (offsetEl) offsetEl.value = '0';
      const cursorEl = document.getElementById('wfList_cursor'); if (cursorEl) cursorEl.value = '';
      const quickSel = document.getElementById('wfList_quick'); if (quickSel) quickSel.value = '';
      const useCursor = document.getElementById('wfList_use_cursor'); if (useCursor) useCursor.checked = false;
      // Reset status chips
      document.querySelectorAll('#wfList_status_chips .chip').forEach(ch=>{ ch.setAttribute('data-selected','0'); ch.style.background=''; });
      // Clear events filter text
      const evt = document.getElementById('wfEvents_filter'); if (evt) evt.value = '';
      wfSaveFilters();
      Toast.success('Filters cleared');
    }catch(e){}
  }
  async function wfListArtifacts(){
    try{
      const id=val('wfRun_run_id');
      const list=await apiClient.makeRequest('GET',`/api/v1/workflows/runs/${id}/artifacts`);
      set('wfRun_result', list);
      renderArtifacts(list);
    }catch(e){ Toast.error(e?.message||e); }
  }
  function renderArtifacts(items){
    const div = document.getElementById('wfRun_artifacts');
    if (!Array.isArray(items) || items.length === 0){ div.innerHTML = '<em>No artifacts</em>'; return; }
    let html = '<h3>Artifacts</h3><ul>';
    items.forEach(a => {
      const id = a.artifact_id || '';
      const uri = a.uri || '';
      const name = (uri && uri.lastIndexOf('/')>=0) ? uri.substring(uri.lastIndexOf('/')+1) : id;
      html += `<li>${name} (${a.mime_type||'?'}, ${a.size_bytes||'?'} bytes) `+
              `<button class="api-button" onclick="wfDownloadArtifact('${id}', '${name.replace(/'/g, "\'")}')">Download</button></li>`;
    });
    html += '</ul>';
    div.innerHTML = html;
  }
  async function wfDownloadArtifact(artifactId, filename){
    try{
      const url = apiClient.baseUrl + `/api/v1/workflows/artifacts/${artifactId}/download`;
      const headers = {};
      if (apiClient.token){
        if (apiClient.authMode === 'single-user') headers['X-API-KEY'] = apiClient.token;
        else if (apiClient.authMode === 'multi-user') headers['Authorization'] = `Bearer ${apiClient.token}`;
        else headers['X-API-KEY'] = apiClient.token;
      }
      const resp = await fetch(url, { headers });
      if (!resp.ok){ throw new Error(`HTTP ${resp.status}`); }
      const blob = await resp.blob();
      const link = document.createElement('a');
      const objUrl = window.URL.createObjectURL(blob);
      link.href = objUrl;
      link.download = filename || 'artifact';
      document.body.appendChild(link);
      link.click();
      link.remove();
      setTimeout(() => URL.revokeObjectURL(objUrl), 1500);
    }catch(e){ Toast.error('Download failed: ' + (e?.message||e)); }
  }
  async function wfPause(){ await wfCtl('pause'); }
  async function wfResume(){ await wfCtl('resume'); }
  async function wfCancel(){ await wfCtl('cancel'); }
  async function wfRetry(){ const id=val('wfRun_run_id'); try{ const r=await apiClient.makeRequest('POST',`/api/v1/workflows/runs/${id}/retry`); Toast.success('Retry requested'); }catch(e){Toast.error(e?.message||e);} }
  async function wfCtl(action){ const id=val('wfRun_run_id'); try{ await apiClient.makeRequest('POST',`/api/v1/workflows/runs/${id}/${action}`); Toast.success(action+' requested'); }catch(e){Toast.error(e?.message||e);} }
  function val(id){ return document.getElementById(id).value.trim(); }
  function set(id,obj){ document.getElementById(id).textContent = JSON.stringify(obj,null,2); }
  function wfApplyPreset(){
    try{
      const v = document.getElementById('wfList_presets').value;
      const afterEl = document.getElementById('wfList_created_after');
      const beforeEl = document.getElementById('wfList_created_before');
      const now = new Date();
      const fmt = (d)=>{ const p=n=>String(n).padStart(2,'0'); return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`; };
      if (v==='today'){
        const start = new Date(); start.setHours(0,0,0,0);
        afterEl.value = fmt(start);
        beforeEl.value = fmt(now);
      } else if (v==='yesterday'){
        const start = new Date(); start.setDate(start.getDate()-1); start.setHours(0,0,0,0);
        const end = new Date(start); end.setHours(23,59,59,999);
        afterEl.value = fmt(start);
        beforeEl.value = fmt(end);
      } else if (v==='lasthour'){
        const start = new Date(now.getTime() - 60*60*1000);
        afterEl.value = fmt(start);
        beforeEl.value = fmt(now);
      } else if (v==='last24h'){
        const start = new Date(now.getTime() - 24*60*60*1000);
        afterEl.value = fmt(start);
        beforeEl.value = fmt(now);
      } else if (v==='thisweek'){
        const day = now.getDay(); // 0=Sun..6=Sat
        const delta = (day + 6) % 7; // days since Monday
        const start = new Date(now);
        start.setDate(now.getDate() - delta);
        start.setHours(0,0,0,0);
        afterEl.value = fmt(start);
        beforeEl.value = fmt(now);
      } else if (v==='last7d'){
        const start = new Date(now.getTime() - 7*24*60*60*1000);
        afterEl.value = fmt(start);
        beforeEl.value = fmt(now);
      } else if (v==='last30d'){
        const start = new Date(now.getTime() - 30*24*60*60*1000);
        afterEl.value = fmt(start);
        beforeEl.value = fmt(now);
      } else {
        afterEl.value = '';
        beforeEl.value = '';
      }
    }catch(e){}
  }
  async function wfDownloadAllServer(){
    try{
      const id = val('wfRun_run_id');
      const url = apiClient.baseUrl + `/api/v1/workflows/runs/${encodeURIComponent(id)}/artifacts/download`;
      const headers = {};
      if (apiClient.token){
        if (apiClient.authMode === 'single-user') headers['X-API-KEY'] = apiClient.token;
        else if (apiClient.authMode === 'multi-user') headers['Authorization'] = `Bearer ${apiClient.token}`;
        else headers['X-API-KEY'] = apiClient.token;
      }
      const resp = await fetch(url, { headers });
      if (!resp.ok){ throw new Error(`HTTP ${resp.status}`); }
      const blob = await resp.blob();
      const link = document.createElement('a');
      const objUrl = window.URL.createObjectURL(blob);
      link.href = objUrl; link.download = `artifacts_${id}.zip`;
      document.body.appendChild(link); link.click(); link.remove();
      setTimeout(()=>URL.revokeObjectURL(objUrl), 1500);
    }catch(e){ Toast.error('Download all (server) failed: '+(e?.message||e)); }
  }
  async function wfDownloadAllClient(){
    try{
      const id=val('wfRun_run_id');
      const list=await apiClient.makeRequest('GET',`/api/v1/workflows/runs/${id}/artifacts`);
      if (!Array.isArray(list) || list.length===0){ Toast.error('No artifacts'); return; }
      // Limit to 25MB client-side to avoid memory pressure
      const MAX_BYTES = 25 * 1024 * 1024;
      let total = 0;
      const files = [];
      for (const a of list){
        const uri = a.uri||'';
        if (!uri.startsWith('file://')) continue;
        const filename = (uri.lastIndexOf('/')>=0) ? uri.substring(uri.lastIndexOf('/')+1) : (a.artifact_id||'file');
        const headers = {};
        if (apiClient.token){
          if (apiClient.authMode === 'single-user') headers['X-API-KEY'] = apiClient.token;
          else if (apiClient.authMode === 'multi-user') headers['Authorization'] = `Bearer ${apiClient.token}`;
          else headers['X-API-KEY'] = apiClient.token;
        }
        const dlUrl = apiClient.baseUrl + `/api/v1/workflows/artifacts/${encodeURIComponent(a.artifact_id)}/download`;
        const resp = await fetch(dlUrl, { headers });
        if (!resp.ok) continue;
        const blob = await resp.blob();
        total += blob.size;
        if (total > MAX_BYTES) { Toast.error('Client zip limit exceeded (25MB). Use server download.'); return; }
        files.push({ name: filename, blob });
      }
      if (files.length===0){ Toast.error('No eligible artifacts for client zip'); return; }
      const zipBlob = await createZipFromFiles(files);
      const link = document.createElement('a');
      const objUrl = URL.createObjectURL(zipBlob);
      link.href = objUrl; link.download = `artifacts_${id}.zip`;
      document.body.appendChild(link); link.click(); link.remove();
      setTimeout(()=>URL.revokeObjectURL(objUrl), 1500);
    }catch(e){ Toast.error('Download all (client) failed: '+(e?.message||e)); }
  }
  // Modal for payload preview
  </script>
</div>
<div id="wfModal" role="dialog" aria-modal="true" aria-hidden="true" aria-labelledby="wfModal_title" data-open="0">
  <div id="wfModal_panel" role="document" tabindex="-1">
    <div class="wf-modal-header">
      <h3 id="wfModal_title">Details</h3>
      <button type="button" class="api-button btn-sm" id="wfModal_close_btn" aria-label="Close dialog">Close</button>
    </div>
    <div id="wfModal_description" class="visually-hidden"></div>
    <div id="wfModal_body" class="wf-modal-body" tabindex="0"></div>
    <div class="wf-modal-footer">
      <button class="api-button" type="button" id="wfModal_copy_btn2" aria-label="Copy modal content">Copy</button>
      <button class="api-button" type="button" id="wfModal_close_btn2">Close</button>
    </div>
  </div>
</div>
<div id="tabWorkflowsApprovals" class="tab-content">
  <h2>Workflows: Approvals</h2>
  <div class="form-group">
    <label>Run ID</label>
    <input type="text" id="wfAppr_run_id" placeholder="uuid" />
    <label>Step ID</label>
    <input type="text" id="wfAppr_step_id" placeholder="step id" />
  </div>
  <div class="form-group">
    <label>Comment</label>
    <input type="text" id="wfAppr_comment" placeholder="optional comment" />
    <label>Edited Fields (JSON)</label>
    <input type="text" id="wfAppr_edited" placeholder='{"final_text":"..."}' />
  </div>
  <div class="form-group">
    <button class="api-button" id="wfApprove_btn">Approve</button>
    <button class="api-button" id="wfReject_btn">Reject</button>
  </div>
  <pre id="wfAppr_result" class="json-viewer"></pre>
  <script>
  async function createZipFromFiles(files){
    const entries = [];
    for (const f of files){ const ab = await f.blob.arrayBuffer(); const data = new Uint8Array(ab); entries.push({ name: f.name, data, crc: crc32(data), size: data.length }); }
    function u16(v){ return new Uint8Array([v & 0xFF, (v>>8)&0xFF]); }
    function u32(v){ return new Uint8Array([v & 0xFF, (v>>8)&0xFF, (v>>16)&0xFF, (v>>24)&0xFF]); }
    function strBytes(s){ return new TextEncoder().encode(s); }
    const fileHeaderSig = u32(0x04034b50), centralHeaderSig = u32(0x02014b50), endSig = u32(0x06054b50);
    const version = u16(20), flags = u16(0), methodStore = u16(0), modTime = u16(0), modDate = u16(0);
    let offset = 0; const chunks = []; const central = [];
    for (const e of entries){
      const nameBytes = strBytes(e.name);
      const lh = [fileHeaderSig, version, flags, methodStore, modTime, modDate, u32(e.crc>>>0), u32(e.size), u32(e.size), u16(nameBytes.length), u16(0)];
      lh.forEach(part=>chunks.push(part)); chunks.push(nameBytes); chunks.push(e.data);
      const localOffset = offset; offset += 30 + nameBytes.length + e.size;
      const ce = [centralHeaderSig, u16(20), version, flags, methodStore, modTime, modDate, u32(e.crc>>>0), u32(e.size), u32(e.size), u16(nameBytes.length), u16(0), u16(0), u16(0), u16(0), u32(0), u32(localOffset), nameBytes];
      central.push(ce);
    }
    const startCentral = offset; for (const ce of central){ ce.forEach(part=>chunks.push(part)); const nameBytes = ce[ce.length-1]; offset += 46 + nameBytes.length; }
    const centralSize = offset - startCentral; const eocd = [endSig, u16(0), u16(0), u16(entries.length), u16(entries.length), u32(centralSize), u32(startCentral), u16(0)];
    eocd.forEach(part=>chunks.push(part));
    const totalLen = chunks.reduce((s,c)=>s+c.length,0); const out = new Uint8Array(totalLen); let pos=0; for(const c of chunks){ out.set(c,pos); pos+=c.length; }
    return new Blob([out], { type: 'application/zip' });
  }
  function crc32(bytes){ let crc=~0; for(let i=0;i<bytes.length;i++){ crc=(crc>>>8)^CRC_TABLE[(crc^bytes[i])&0xFF]; } return (~crc)>>>0; }
  const CRC_TABLE = (()=>{ const t=new Uint32Array(256); for(let n=0;n<256;n++){ let c=n; for(let k=0;k<8;k++) c=(c&1)?(0xEDB88320^(c>>>1)):(c>>>1); t[n]=c>>>0; } return t; })();
  </script>
</div>

<div id="tabWorkflowsApprovals" class="tab-content">
  <h2>Workflows: Approvals</h2>
  <div class="form-group">
    <label>Run ID</label>
    <input type="text" id="wfAppr_run_id" placeholder="uuid" />
    <label>Step ID</label>
    <input type="text" id="wfAppr_step_id" placeholder="step id" />
  </div>
  <div class="form-group">
    <label>Comment</label>
    <input type="text" id="wfAppr_comment" placeholder="optional comment" />
    <label>Edited Fields (JSON)</label>
    <input type="text" id="wfAppr_edited" placeholder='{"final_text":"..."}' />
  </div>
  <div class="form-group">
    <button class="api-button" id="wfApprove_btn2">Approve</button>
    <button class="api-button" id="wfReject_btn2">Reject</button>
  </div>
  <pre id="wfAppr_result" class="json-viewer"></pre>
  <script>
  async function wfApprove(){ await wfHuman('approve'); }
  async function wfReject(){ await wfHuman('reject'); }
  async function wfHuman(action){
    try{
      const run = document.getElementById('wfAppr_run_id').value.trim();
      const step = document.getElementById('wfAppr_step_id').value.trim();
      const comment = document.getElementById('wfAppr_comment').value;
      const edited = document.getElementById('wfAppr_edited').value;
      let body = { comment: comment||null };
      if (edited) { try{ body.edited_fields = JSON.parse(edited);}catch(e){} }
      const resp = await apiClient.makeRequest('POST',`/api/v1/workflows/runs/${run}/steps/${step}/${action}`,{ body });
      document.getElementById('wfAppr_result').textContent = JSON.stringify(resp,null,2);
      Toast.success('Sent '+action);
    }catch(e){ Toast.error('Action failed: '+(e?.message||e)); }
  }
  function escapeHtml(s){ return (s||'').replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;'}[m])); }
</script>
</div>
<script>
  const WF_RUN_STATUS_COLORS = {
    queued: '#888',
    running: '#0a84ff',
    paused: '#f0ad4e',
    waiting_human: '#6f42c1',
    waiting_approval: '#8e44ad',
    succeeded: '#2e7d32',
    failed: '#c62828',
    cancelled: '#616161'
  };
  function wfRunStatusChip(status){
    const key = String(status||'').toLowerCase();
    const color = WF_RUN_STATUS_COLORS[key] || '#555';
    const label = key || 'unknown';
    return `<span class="wf-run-status-chip" data-status="${label}" title="Status: ${label}" style="background:${color};color:#fff;">${label}</span>`;
  }
  const wfRunVirtualState = {
    runs: [],
    container: null,
    body: null,
    topPad: null,
    bottomPad: null,
    rowHeight: 56,
    buffer: 8,
    lastStart: -1,
    lastEnd: -1,
    onScroll: null,
    resizeHandler: null
  };
  function wfRunVirtualCleanup(){
    try{
      const st = wfRunVirtualState;
      if (st.container && st.onScroll){ st.container.removeEventListener('scroll', st.onScroll); }
      if (st.resizeHandler){ window.removeEventListener('resize', st.resizeHandler); }
    }catch(_){}
    wfRunVirtualState.container = null;
    wfRunVirtualState.body = null;
    wfRunVirtualState.topPad = null;
    wfRunVirtualState.bottomPad = null;
    wfRunVirtualState.onScroll = null;
    wfRunVirtualState.resizeHandler = null;
    wfRunVirtualState.lastStart = -1;
    wfRunVirtualState.lastEnd = -1;
    wfRunVirtualState.runs = [];
  }
  function wfRunRowHtml(run){
    const ridRaw = run?.run_id ?? '';
    const rid = String(ridRaw);
    const safeRid = rid.replace(/'/g, "\\'");
    const wf = run?.workflow_id ?? '';
    const owner = run?.user_id ?? '';
    const statusChip = wfRunStatusChip(run?.status);
    const created = run?.created_at || '';
    const ended = run?.ended_at || '';
    return `<tr data-run-row="1">
      <td style="font-family:monospace"><a href="#" onclick="document.getElementById('wfRun_run_id').value='${safeRid}'; wfGetRun(); return false;">${escapeHtml(rid)}</a></td>
      <td>${escapeHtml(String(wf ?? ''))}</td>
      <td>${escapeHtml(String(owner ?? ''))}</td>
      <td>${statusChip}</td>
      <td>${escapeHtml(created)}</td>
      <td>${escapeHtml(ended)}</td>
      <td><button class="api-button btn-sm" onclick="wfGetEventsFor('${safeRid}')">Events</button></td>
    </tr>`;
  }
  function wfRunVirtualInit(runs){
    const host = document.getElementById('wfRun_list');
    if (!host) return;
    wfRunVirtualCleanup();
    wfRunVirtualState.runs = Array.isArray(runs) ? runs.slice() : [];
    host.innerHTML = `
      <div class="wf-runs-container" id="wfRuns_table_container" role="region" aria-label="Workflow runs results">
        <table class="wf-runs-table">
          <thead>
            <tr>
              <th scope="col">Run ID</th>
              <th scope="col">Workflow</th>
              <th scope="col">Owner</th>
              <th scope="col">Status</th>
              <th scope="col">Created</th>
              <th scope="col">Ended</th>
              <th scope="col">Actions</th>
            </tr>
          </thead>
          <tbody id="wfRuns_table_body">
            <tr class="wf-run-pad" data-pad="top"><td colspan="7" style="height:0;padding:0;border:none"></td></tr>
            <tr class="wf-run-pad" data-pad="bottom"><td colspan="7" style="height:0;padding:0;border:none"></td></tr>
          </tbody>
        </table>
      </div>`;
    const container = document.getElementById('wfRuns_table_container');
    const body = document.getElementById('wfRuns_table_body');
    const pads = body ? body.querySelectorAll('.wf-run-pad') : [];
    wfRunVirtualState.container = container;
    wfRunVirtualState.body = body;
    wfRunVirtualState.topPad = pads[0] || null;
    wfRunVirtualState.bottomPad = pads[1] || null;
    wfRunVirtualState.lastStart = -1;
    wfRunVirtualState.lastEnd = -1;
    if (container){
      const onScroll = ()=>wfRunVirtualRender();
      container.addEventListener('scroll', onScroll);
      wfRunVirtualState.onScroll = onScroll;
    }
    if (!wfRunVirtualState.resizeHandler){
      const handler = ()=>wfRunVirtualRender(true);
      window.addEventListener('resize', handler);
      wfRunVirtualState.resizeHandler = handler;
    }
    wfRunVirtualRender(true);
    setTimeout(()=>{
      try{
        const sample = wfRunVirtualState.body?.querySelector('tr[data-run-row]');
        if (sample){
          const h = sample.getBoundingClientRect().height;
          if (h && !Number.isNaN(h)){ wfRunVirtualState.rowHeight = h; wfRunVirtualRender(true); }
        }
      }catch(_){}
    }, 40);
  }
  function wfRunVirtualRender(force = false){
    try{
      const st = wfRunVirtualState;
      const runs = st.runs;
      const container = st.container;
      const body = st.body;
      if (!container || !body || !Array.isArray(runs) || !runs.length){
        return;
      }
      const viewport = container.clientHeight || 360;
      const rowHeight = st.rowHeight || 56;
      const buffer = st.buffer || 8;
      const scrollTop = container.scrollTop || 0;
      let start = Math.floor(scrollTop / rowHeight) - buffer;
      if (start < 0) start = 0;
      let end = start + Math.ceil(viewport / rowHeight) + buffer * 2;
      if (end > runs.length) end = runs.length;
      if (!force && st.lastStart === start && st.lastEnd === end){
        return;
      }
      st.lastStart = start;
      st.lastEnd = end;
      const topPadHeight = start * rowHeight;
      const bottomPadHeight = (runs.length - end) * rowHeight;
      if (st.topPad){
        const td = st.topPad.querySelector('td');
        if (td){ td.style.height = `${topPadHeight}px`; }
      }
      if (st.bottomPad){
        const td = st.bottomPad.querySelector('td');
        if (td){ td.style.height = `${bottomPadHeight}px`; }
      }
      body.querySelectorAll('tr[data-run-row]').forEach(row => row.remove());
      const slice = runs.slice(start, end);
      const html = slice.map(wfRunRowHtml).join('');
      if (st.bottomPad){
        st.bottomPad.insertAdjacentHTML('beforebegin', html);
      } else {
        body.insertAdjacentHTML('beforeend', html);
      }
      const sample = body.querySelector('tr[data-run-row]');
      if (sample){
        const h = sample.getBoundingClientRect().height;
        if (h && !Number.isNaN(h) && Math.abs(h - st.rowHeight) > 1){
          st.rowHeight = h;
        }
      }
      try{ wfAppendWebhookButtons(); }catch(_){}
    }catch(_){}
  }
  function renderRunTable(resp){
    try{
      const runs = Array.isArray(resp?.runs) ? resp.runs : [];
      if (!runs.length){
        wfRunVirtualCleanup();
        document.getElementById('wfRun_list').innerHTML = '<em>No runs</em>';
        return;
      }
      wfRunVirtualInit(runs);
    }catch(e){
      wfRunVirtualCleanup();
      document.getElementById('wfRun_list').innerHTML = '<em>Error rendering</em>';
    }
  }
  async function wfGetEventsFor(runId){
    try{
      const r = await apiClient.makeRequest('GET', `/api/v1/workflows/runs/${runId}/events`);
      set('wfRun_result', r);
    }catch(e){ Toast.error('Events failed: '+(e?.message||e)); }
  }
  function wfAppendWebhookButtons(){
    try{
      const container = document.getElementById('wfRun_list');
      if (!container) return;
      const rows = container.querySelectorAll('tbody tr');
      rows.forEach(tr => {
        const cells = tr.querySelectorAll('td');
        if (cells.length < 7) return;
        const actions = cells[cells.length-1];
        // If already has Webhooks button, skip
        if (actions && actions.textContent.includes('Webhooks')) return;
        // Derive run_id from the link in first cell
        let rid = '';
        try{
          const a = tr.querySelector('td a[href="#"]');
          if (a){ rid = a.textContent.trim(); }
        }catch(_){ rid = ''; }
        if (!rid) return;
        const btn = document.createElement('button');
        btn.className = 'api-button btn-sm';
        btn.textContent = 'Webhooks';
        btn.title = 'Show webhook delivery history';
        btn.addEventListener('click', () => wfShowWebhookDeliveries(rid));
        actions.appendChild(document.createTextNode(' '));
        actions.appendChild(btn);
      });
    }catch(_){ }
  }
  async function wfShowWebhookDeliveries(runId){
    try{
      const r = await apiClient.makeRequest('GET', `/api/v1/workflows/runs/${runId}/webhooks/deliveries`);
      const items = (r && r.deliveries) ? r.deliveries : [];
      const rows = items.map(d=>{
        const statusPayload = { status: d.status };
        const statusHtml = wfRenderWebhookStatus('webhook_delivery', statusPayload) || `<span class="wf-badge info">${escapeHtml(String(d.status||'unknown'))}</span>`;
        return `<tr><td>${d.event_seq ?? '-'}</td><td>${escapeHtml(d.created_at||'')}</td><td>${escapeHtml(d.host||'')}</td><td>${statusHtml}</td><td>${d.code != null ? escapeHtml(String(d.code)) : ''}</td></tr>`;
      }).join('');
      const copyText = items.length ? items.map(d=>`#${d.event_seq} ${d.created_at} ${d.host||''} ${d.status||''} ${d.code||''}`).join('\n') : '(no deliveries)';
      const html = items.length
        ? `<table class="wf-delivery-table"><thead><tr><th>Seq</th><th>Created</th><th>Host</th><th>Status</th><th>Code</th></tr></thead><tbody>${rows}</tbody></table>`
        : '<div class="wf-diff-none">No webhook deliveries recorded for this run.</div>';
      wfModalShow({
        title: 'Webhook deliveries',
        description: `Run ${runId}`,
        bodyHtml: html,
        wrapPre: false,
        copyText,
        focusSelector: '#wfModal_copy_btn'
      });
    }catch(e){ Toast.error('Deliveries failed: '+(e?.message||e)); }
  }
</script>
<script>
  function wfDiffObjects(before, after, path = [], out = []) {
    if (before === after) return out;
    const isObjA = before !== null && typeof before === 'object';
    const isObjB = after !== null && typeof after === 'object';
    const isArrA = Array.isArray(before);
    const isArrB = Array.isArray(after);
    if (isArrA && isArrB){
      const max = Math.max(before.length, after.length);
      for (let i=0;i<max;i++){
        if (i >= before.length){ out.push({ path: path.concat(i), before: undefined, after: after[i] }); }
        else if (i >= after.length){ out.push({ path: path.concat(i), before: before[i], after: undefined }); }
        else { wfDiffObjects(before[i], after[i], path.concat(i), out); }
      }
      return out;
    }
    if (isArrA !== isArrB){
      out.push({ path, before, after });
      return out;
    }
    if (isObjA && isObjB){
      const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
      keys.forEach(key=>{
        if (!Object.prototype.hasOwnProperty.call(after, key)){
          out.push({ path: path.concat(key), before: before[key], after: undefined });
        } else if (!Object.prototype.hasOwnProperty.call(before, key)){
          out.push({ path: path.concat(key), before: undefined, after: after[key] });
        } else {
          wfDiffObjects(before[key], after[key], path.concat(key), out);
        }
      });
      return out;
    }
    out.push({ path, before, after });
    return out;
  }
  function wfFormatDiffPath(path){
    if (!Array.isArray(path) || !path.length) return '(root)';
    return path.reduce((acc, seg)=>{
      if (typeof seg === 'number'){ return `${acc}[${seg}]`; }
      return acc ? `${acc}.${seg}` : String(seg);
    }, '');
  }
  function wfDiffValueSummary(value){
    if (value === undefined) return '-';
    if (value === null) return 'null';
    if (typeof value === 'string') return value;
    try{
      return JSON.stringify(value, null, 2);
    }catch(_){
      return String(value);
    }
  }
  function wfRenderWebhookStatus(eventType, payload){
    try{
      if (String(eventType||'').toLowerCase() !== 'webhook_delivery' || !payload) return '';
      const statusRaw = payload.status || payload.delivery_status || '';
      const status = String(statusRaw||'').toLowerCase();
      const label = status ? status.replace(/_/g,' ') : 'delivery';
      const code = payload.code != null ? `HTTP ${payload.code}` : '';
      const host = payload.host ? escapeHtml(String(payload.host)) : '';
      let cls = 'info';
      if (status.includes('fail') || status.includes('error')) cls = 'danger';
      else if (status.includes('retry') || status.includes('pending')) cls = 'warn';
      else if (status.includes('ok') || status.includes('success')) cls = 'success';
      const badge = `<span class="wf-badge ${cls}" title="Delivery status">${escapeHtml(label)}</span>`;
      const parts = [badge];
      if (code) parts.push(`<span class="wf-webhook-status-code">${escapeHtml(code)}</span>`);
      if (host) parts.push(`<span class="wf-webhook-status-host">${host}</span>`);
      return `<div class="wf-webhook-status">${parts.join('<span style="opacity:0.5">•</span>')}</div>`;
    }catch(_){
      return '';
    }
  }
  function wfRenderEventTypeChips(types){
    try{
      const container = document.getElementById('wfEvents_type_chips');
      if (!container) return;
      if (!Array.isArray(types) || !types.length){ container.innerHTML = ''; return; }
      const currentRaw = (document.getElementById('wfEvents_filter')?.value||'').trim();
      const currentSet = new Set(currentRaw ? currentRaw.split(',').map(s=>s.trim().toLowerCase()).filter(Boolean) : []);
      const html = types.slice().sort().map(t=>{
        const lower = String(t).toLowerCase();
        const active = currentSet.has(lower);
        const label = escapeHtml(String(t));
        return `<span class="wf-chip${active?' active':''}" data-type="${label}" role="button" tabindex="0" onclick="wfToggleEventTypeChip(this)" onkeydown="wfEventTypeChipKey(event,this)">${label}</span>`;
      }).join('');
      container.innerHTML = html;
    }catch(_){ }
  }
  function wfSyncEventTypeChips(){
    try{
      if (wfEventsState && wfEventsState.types){
        wfRenderEventTypeChips(Array.from(wfEventsState.types));
      }
    }catch(_){ }
  }
  function wfToggleEventTypeChip(el){
    try{
      if (!el) return;
      const type = (el.getAttribute('data-type')||'').trim();
      if (!type) return;
      const input = document.getElementById('wfEvents_filter');
      if (!input) return;
      const values = input.value.split(',').map(s=>s.trim()).filter(Boolean);
      const lower = type.toLowerCase();
      const set = new Set(values.map(v=>v.toLowerCase()));
      if (set.has(lower)){
        const filtered = values.filter(v=>v.toLowerCase() !== lower);
        input.value = filtered.join(',');
        el.classList.remove('active');
      } else {
        values.push(type);
        input.value = values.join(',');
        el.classList.add('active');
      }
      const runId = (document.getElementById('wfRun_run_id')?.value||'').trim();
      if (runId){
        wfGetEvents();
      }
    }catch(_){ }
  }
  function wfEventTypeChipKey(ev, el){
    try{
      if (ev.key === 'Enter' || ev.key === ' '){
        ev.preventDefault();
        wfToggleEventTypeChip(el);
      }
    }catch(_){ }
  }
  function wfScrollEventsToBottom(){
    try{
      const host = document.getElementById('wfEvents_table');
      if (host){ host.scrollTop = host.scrollHeight; }
    }catch(_){ }
  }
  function wfCopyEventsCursor(){
    try{
      const val = document.getElementById('wfEvents_next_cursor_value')?.value || '';
      if (!val){ Toast.error('No cursor available'); return; }
      navigator.clipboard.writeText(val).then(()=>Toast.success('Copied')).catch(()=>Toast.error('Copy failed'));
    }catch(e){ Toast.error('Copy failed: '+(e?.message||e)); }
  }
  function wfToggleTail(){
    try{
      const follow = !!document.getElementById('wfEvents_tail')?.checked;
      wfEventsState.followTail = follow;
      if (follow){ wfScrollEventsToBottom(); }
      if (typeof Utils !== 'undefined'){ Utils.saveToStorage('workflows-events-tail', { follow }); }
    }catch(_){ }
  }
  function wfRestoreTailPreference(){
    try{
      const saved = (typeof Utils !== 'undefined') ? Utils.getFromStorage('workflows-events-tail') : null;
      const follow = !!(saved && saved.follow);
      wfEventsState.followTail = follow;
      const chk = document.getElementById('wfEvents_tail');
      if (chk) chk.checked = follow;
    }catch(_){ }
  }
  function wfApplyErrorFilter(){
    try{
      const input = document.getElementById('wfEvents_filter');
      if (input){
        input.value = ['step_failed','run_failed','run_cancelled','step_timeout','webhook_delivery_failed'].join(',');
      }
      if (wfEventsState && wfEventsState.types){
        wfRenderEventTypeChips(Array.from(wfEventsState.types));
      }
      const runId = (document.getElementById('wfRun_run_id')?.value||'').trim();
      if (runId){
        wfGetEvents();
      } else {
        Toast.info('Set a run id, then choose "Get Events" to refresh.');
      }
    }catch(_){ }
  }
  function wfRenderEventsTable(items, latencyMs, append){
    const info = document.getElementById('wfEvents_info');
    const tbl = document.getElementById('wfEvents_table');
    const colors = {
      run_started:'#0a84ff', step_started:'#0070c9', step_completed:'#2e7d32', step_failed:'#c62828',
      step_timeout:'#f0ad4e', waiting_human:'#6f42c1', run_cancelled:'#616161', run_failed:'#c62828', run_completed:'#2e7d32',
      heartbeat:'#888', snapshot:'#555'
    };
    function chip(type){ const c = colors[type]||'#555'; return `<span style="display:inline-block;padding:2px 6px;border-radius:10px;background:${c};color:#fff;font-size:11px">${type}</span>`; }
    if (!append){
      tbl.innerHTML = '<table style="width:100%;border-collapse:collapse" id="wfEvents_table_inner"><thead><tr><th style="text-align:left;padding:4px;border-bottom:1px solid var(--color-base-40)">Seq</th><th style="text-align:left;padding:4px;border-bottom:1px solid var(--color-base-40)">Type</th><th style="text-align:left;padding:4px;border-bottom:1px solid var(--color-base-40)">Created</th><th style="text-align:left;padding:4px;border-bottom:1px solid var(--color-base-40)">Payload</th></tr></thead><tbody></tbody></table>';
      wfEventsState.types = new Set();
    }
    const tbody = document.querySelector('#wfEvents_table_inner tbody');
    const fRaw = (document.getElementById('wfEvents_filter')?.value||'').trim();
    const fSet = new Set((fRaw? fRaw.split(','): []).map(s=>s.trim().toLowerCase()).filter(Boolean));
    let lastSeq = wfEventsState.lastSeq || null;
    let added = 0;
    const typeSet = new Set();
    (Array.isArray(items)? items: []).forEach(e => {
      const typeOriginal = String(e.event_type||'');
      const etype = typeOriginal.toLowerCase();
      if (fSet.size && !fSet.has(etype)) return;
      typeSet.add(typeOriginal);
      const tr = document.createElement('tr');
      const key = String(wfEventsState.runId||'')+':'+String(e.event_seq);
      const payloadStr = JSON.stringify(e.payload||{}, null, 2);
      wfPayloadCache[key] = payloadStr;
      const preview = payloadStr.length > 160 ? escapeHtml(payloadStr.slice(0,160)) + '…' : escapeHtml(payloadStr);
      const viewBtn = payloadStr.length > 160 ? ` <button class=\"api-button\" onclick=\"wfOpenModalPayload('${key}')\">View</button>` : '';
      const meta = wfRenderWebhookStatus(etype, e.payload || {});
      tr.innerHTML = `<td style=\"padding:4px;border-bottom:1px solid var(--color-base-30)\">${e.event_seq}</td>`+
                     `<td style=\"padding:4px;border-bottom:1px solid var(--color-base-30)\">${chip(typeOriginal)}</td>`+
                     `<td style=\"padding:4px;border-bottom:1px solid var(--color-base-30)\">${e.created_at||''}</td>`+
                     `<td style=\"padding:4px;border-bottom:1px solid var(--color-base-30)\"><code style=\"font-size:11px\">${preview}</code>${viewBtn}${meta}</td>`;
      tbody.appendChild(tr);
      lastSeq = e.event_seq;
      added += 1;
    });
    if (!append){ wfRenderEventTypeChips(Array.from(typeSet)); }
    typeSet.forEach(t=>wfEventsState.types.add(t));
    const msg = `events ${added} • last_seq ${lastSeq!=null?lastSeq:'-'} • fetch ${latencyMs||0}ms` + (wfEventsState.nextCursor ? ' • next_cursor ready' : '');
    info.textContent = msg;
    wfRenderEventTypeChips(Array.from(wfEventsState.types));
    const cursorInput = document.getElementById('wfEvents_next_cursor_value');
    if (cursorInput){ cursorInput.value = wfEventsState.nextCursor || ''; }
    const cursorBtn = document.getElementById('wfEvents_copy_cursor');
    if (cursorBtn){ cursorBtn.disabled = !wfEventsState.nextCursor; }
    if (wfEventsState.followTail){ wfScrollEventsToBottom(); }
  }
  const wfModalState = {
    opener: null,
    copyHandler: null,
    copyText: ''
  };
  function wfModalShow(options = {}){
    try{
      const modal = document.getElementById('wfModal');
      const panel = document.getElementById('wfModal_panel');
      const body = document.getElementById('wfModal_body');
      const titleEl = document.getElementById('wfModal_title');
      const descEl = document.getElementById('wfModal_description');
      const copyBtn = document.getElementById('wfModal_copy_btn');
      if (!modal || !panel || !body || !titleEl || !copyBtn){ return; }
      const {
        title = 'Details',
        bodyText = '',
        bodyHtml = null,
        bodyNode = null,
        wrapPre = true,
        description = '',
        copyText = null,
        copyHandler = null,
        focusSelector = null
      } = options;
      titleEl.textContent = title;
      if (description){
        descEl.textContent = description;
        descEl.style.display = 'block';
        panel.setAttribute('aria-describedby', 'wfModal_description');
      } else {
        descEl.textContent = '';
        descEl.style.display = 'none';
        panel.removeAttribute('aria-describedby');
      }
      body.innerHTML = '';
      const isDomNode = (typeof Node !== 'undefined') && (bodyNode instanceof Node);
      if (isDomNode){
        body.appendChild(bodyNode);
      } else if (bodyHtml != null){
        body.innerHTML = bodyHtml;
      } else if (wrapPre){
        const pre = document.createElement('pre');
        pre.className = 'json-viewer';
        pre.textContent = bodyText || '';
        body.appendChild(pre);
      } else {
        const div = document.createElement('div');
        div.textContent = bodyText || '';
        body.appendChild(div);
      }
      body.scrollTop = 0;
      wfModalState.copyHandler = (typeof copyHandler === 'function') ? copyHandler : null;
      const computedCopy = copyText != null ? copyText : body.textContent;
      wfModalState.copyText = computedCopy || '';
      const copyAvailable = !!(wfModalState.copyHandler || wfModalState.copyText);
      copyBtn.style.display = copyAvailable ? 'inline-flex' : 'none';
      copyBtn.disabled = !copyAvailable;
      copyBtn.setAttribute('aria-hidden', copyAvailable ? 'false' : 'true');
      wfModalState.opener = document.activeElement;
      modal.dataset.open = '1';
      modal.setAttribute('aria-hidden','false');
      modal.style.display = 'flex';
      const ensureFocus = ()=>{
        try{
          if (focusSelector){
            const chosen = panel.querySelector(focusSelector);
            if (chosen && !chosen.hasAttribute('disabled') && chosen.offsetParent !== null){
              chosen.focus();
              return;
            }
          }
        }catch(_){}
        try{
          const focusables = Array.from(panel.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')).filter(el=>!el.hasAttribute('disabled') && panel.contains(el));
          if (focusables.length){ focusables[0].focus(); return; }
        }catch(_){}
        panel.focus();
      };
      setTimeout(ensureFocus, 15);
      const getFocusables = ()=>Array.from(panel.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')).filter(el=>{
        if (!panel.contains(el) || el.hasAttribute('disabled')) return false;
        const hidden = el.offsetParent === null && (!el.getClientRects || el.getClientRects().length === 0);
        return !hidden;
      });
      const onKey = (ev)=>{
        if (ev.key === 'Escape'){ wfCloseModal(); return; }
        if (ev.key === 'Tab'){
          const focusables = getFocusables();
          if (!focusables.length){ ev.preventDefault(); panel.focus(); return; }
          const first = focusables[0];
          const last = focusables[focusables.length-1];
          const active = document.activeElement;
          if (ev.shiftKey){
            if (active === first || !panel.contains(active)){
              ev.preventDefault();
              last.focus();
            }
          } else {
            if (active === last){
              ev.preventDefault();
              first.focus();
            }
          }
        }
      };
      const onClick = (ev)=>{ if (ev.target === modal){ wfCloseModal(); } };
      document.addEventListener('keydown', onKey);
      modal.addEventListener('click', onClick);
      modal._wfOnKey = onKey;
      modal._wfOnClick = onClick;
    }catch(_){}
  }
  function wfOpenModalPayload(key){
    try{
      const raw = wfPayloadCache[key] || '';
      const parts = String(key||'').split(':');
      const seq = parts.length > 1 ? parseInt(parts[1], 10) : null;
      const rid = parts[0] || '';
      let parsed = null;
      let compactText = raw;
      let diffCopy = 'Diff unavailable';
      let diffHtml = '<div class="wf-diff-none">Diff unavailable (non-JSON payload or no prior event)</div>';
      try{
        parsed = JSON.parse(raw);
        compactText = JSON.stringify(parsed);
      }catch(_){
        parsed = null;
        compactText = raw;
      }
      let diffs = [];
      if (parsed && seq !== null && seq > 0){
        const prevKey = `${rid}:${seq-1}`;
        const prevRaw = wfPayloadCache[prevKey];
        if (prevRaw){
          try{
            const prevParsed = JSON.parse(prevRaw);
            diffs = wfDiffObjects(prevParsed, parsed, [], []);
            if (diffs.length){
              const limited = diffs.slice(0, 200);
              const rows = limited.map(d=>`<tr><td>${escapeHtml(wfFormatDiffPath(d.path))}</td><td><pre>${escapeHtml(wfDiffValueSummary(d.before))}</pre></td><td><pre>${escapeHtml(wfDiffValueSummary(d.after))}</pre></td></tr>`).join('');
              diffHtml = `<table class="wf-diff-table"><thead><tr><th>Path</th><th>Previous</th><th>Current</th></tr></thead><tbody>${rows}</tbody></table>`;
              if (diffs.length > 200){
                diffHtml += `<div class="wf-diff-none">+${diffs.length-200} more differences truncated</div>`;
              }
              diffCopy = diffs.map(d=>`${wfFormatDiffPath(d.path)} :: ${wfDiffValueSummary(d.before)} → ${wfDiffValueSummary(d.after)}`).join('\n');
            } else {
              diffHtml = '<div class="wf-diff-none">No differences compared to previous event.</div>';
              diffCopy = 'No differences compared to previous event.';
            }
          }catch(_){
            diffHtml = '<div class="wf-diff-none">Diff unavailable (previous payload not JSON)</div>';
          }
        }
      }
      const container = document.createElement('div');
      container.style.display = 'flex';
      container.style.flexDirection = 'column';
      container.style.gap = '8px';
      const toolbar = document.createElement('div');
      toolbar.className = 'wf-modal-toolbar';
      const prettyBtn = document.createElement('button');
      prettyBtn.className = 'api-button btn-sm';
      prettyBtn.textContent = 'Pretty';
      prettyBtn.setAttribute('data-view','pretty');
      const compactBtn = document.createElement('button');
      compactBtn.className = 'api-button btn-sm';
      compactBtn.textContent = 'Compact';
      compactBtn.setAttribute('data-view','compact');
      const diffBtn = document.createElement('button');
      diffBtn.className = 'api-button btn-sm';
      diffBtn.textContent = 'Diff';
      diffBtn.setAttribute('data-view','diff');
      compactBtn.disabled = compactText === raw;
      diffBtn.disabled = diffHtml.includes('Diff unavailable');
      toolbar.append(prettyBtn, compactBtn, diffBtn);
      const pre = document.createElement('pre');
      pre.className = 'json-viewer';
      const diffContainer = document.createElement('div');
      diffContainer.innerHTML = diffHtml;
      diffContainer.style.display = 'none';
      const note = document.createElement('div');
      note.style.fontSize = '12px';
      note.style.color = 'var(--color-base-0)';
      if (seq !== null && seq > 0){
        note.textContent = `Comparing to event #${seq-1}`;
      } else {
        note.textContent = '';
      }
      const buttonList = [prettyBtn, compactBtn, diffBtn];
      let currentMode = 'pretty';
      const setActive = ()=>{
        buttonList.forEach(btn=>{
          btn.classList.toggle('active', btn.getAttribute('data-view') === currentMode);
        });
      };
      const copyHandler = ()=>{
        const text = currentMode === 'diff' ? diffCopy : (currentMode === 'compact' ? compactText : raw);
        if (!text){
          Toast.error('Nothing to copy');
          return;
        }
        navigator.clipboard.writeText(text).then(()=>Toast.success('Copied')).catch(()=>Toast.error('Copy failed'));
      };
      const setView = (mode)=>{
        currentMode = mode;
        if (mode === 'diff'){
          pre.style.display = 'none';
          diffContainer.style.display = 'block';
          wfModalState.copyText = diffCopy;
        } else {
          diffContainer.style.display = 'none';
          pre.style.display = 'block';
          if (mode === 'compact'){
            pre.textContent = compactText;
            wfModalState.copyText = compactText;
          } else {
            pre.textContent = raw || '(empty payload)';
            wfModalState.copyText = raw;
          }
        }
        setActive();
      };
      prettyBtn.addEventListener('click', ()=>setView('pretty'));
      compactBtn.addEventListener('click', ()=>{ if (!compactBtn.disabled) setView('compact'); });
      diffBtn.addEventListener('click', ()=>{ if (!diffBtn.disabled) setView('diff'); });
      container.appendChild(toolbar);
      if (note.textContent){ container.appendChild(note); }
      container.appendChild(pre);
      container.appendChild(diffContainer);
      wfModalShow({
        title: seq != null ? `Event #${seq}` : 'Event payload',
        description: rid ? `Run ${rid}` : '',
        bodyNode: container,
        wrapPre: false,
        copyHandler,
        focusSelector: '.wf-modal-toolbar button[data-view="pretty"]'
      });
      setView('pretty');
    }catch(_){}
  }
  function wfCloseModal(){
    try{
      const modal = document.getElementById('wfModal');
      if (!modal) return;
      modal.dataset.open = '0';
      modal.setAttribute('aria-hidden','true');
      modal.style.display = 'none';
      if (modal._wfOnKey){ document.removeEventListener('keydown', modal._wfOnKey); modal._wfOnKey = null; }
      if (modal._wfOnClick){ modal.removeEventListener('click', modal._wfOnClick); modal._wfOnClick = null; }
      const panel = document.getElementById('wfModal_panel');
      if (panel){ panel.blur(); }
      const opener = wfModalState.opener;
      wfModalState.opener = null;
      wfModalState.copyHandler = null;
      wfModalState.copyText = '';
      if (opener && typeof opener.focus === 'function'){ opener.focus(); }
    }catch(_){}
  }
  function wfCopyModal(){
    try{
      if (wfModalState.copyHandler){
        wfModalState.copyHandler();
        return;
      }
      const text = wfModalState.copyText || document.getElementById('wfModal_body')?.textContent || '';
      if (!text){
        Toast.error('Nothing to copy');
        return;
      }
      navigator.clipboard.writeText(text).then(()=>Toast.success('Copied')).catch(()=>Toast.error('Copy failed'));
    }catch(e){ Toast.error('Copy failed: '+(e?.message||e)); }
  }
  window.setTimeout(()=>wfRoutingRefreshOptions(false), 0);
  function escapeHtml(s){ return (s||'').replace(/[&<>\"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;','\'':'&#39;'}[m])); }
</script>
<script>
  // Auto-load Templates list when the Workflows Definitions tab becomes active
  (function(){
    try{
      const target = document.getElementById('tabWorkflowsDefinitions');
      if (!target) return;
      const trigger = () => { try { wfTplInit(); } catch(e) { /* ignore */ } };
      // If already active (first open), trigger immediately
      if (target.classList.contains('active')) { trigger(); }
      // Observe class changes to detect activation
      const obs = new MutationObserver((muts)=>{
        for (const m of muts){
          if (m.attributeName === 'class' && target.classList.contains('active')){
            trigger();
          }
        }
      });
      obs.observe(target, { attributes: true });
      // Cleanup on unload
      window.addEventListener('beforeunload', ()=>{ try{ obs.disconnect(); }catch(_){} });
    }catch(e){ /* ignore */ }
  })();
  async function wfTplInit(){
    try{
      await wfTplLoadTags();
      wfTplRestorePrefs();
      await wfTplLoadList();
    }catch(e){ /* ignore */ }
  }
  async function wfTplLoadTags(){
    try{
      const tagSel = document.getElementById('wfTpl_tag');
      if (!tagSel) return;
      // load only once per session
      if (tagSel._wf_loaded_tags) return;
      const tags = await apiClient.makeRequest('GET','/api/v1/workflows/templates/tags');
      const arr = Array.isArray(tags)? tags : [];
      tagSel.innerHTML = '<option value="">(all)</option>' + arr.map(t=>`<option value="${t}">${t}</option>`).join('');
      tagSel._wf_loaded_tags = true;
      // save on change
      try { if (!tagSel._wf_onchange){ tagSel.addEventListener('change', ()=>{ wfTplSavePrefs(); wfTplLoadList(); }); tagSel._wf_onchange = true; } } catch(_){ }
    }catch(e){ /* ignore */ }
  }
  // Debounced search for templates
  let _wfTplDebounce = null;
  function wfTplQueryChanged(){
    try{
      wfTplSavePrefs();
      if (_wfTplDebounce){ clearTimeout(_wfTplDebounce); }
      _wfTplDebounce = setTimeout(()=>{ try{ wfTplLoadList(); }catch(_){ } }, 350);
    }catch(_){ }
  }
  function wfTplSavePrefs(){
    try{
      const q = (document.getElementById('wfTpl_search')?.value||'').trim();
      const tag = (document.getElementById('wfTpl_tag')?.value||'').trim();
      if (typeof Utils !== 'undefined') Utils.saveToStorage('workflows-templates-filters', { q, tag });
    }catch(_){ }
  }
  function wfTplRestorePrefs(){
    try{
      const saved = (typeof Utils !== 'undefined') ? Utils.getFromStorage('workflows-templates-filters') : null;
      if (!saved) return;
      try{ const el=document.getElementById('wfTpl_search'); if (el) el.value = saved.q || ''; }catch(_){ }
      try{ const el=document.getElementById('wfTpl_tag'); if (el && saved.tag){ el.value = saved.tag; } }catch(_){ }
    }catch(_){ }
  }
  const WF_LOCAL_TEMPLATES_KEY = 'workflows-local-templates';
  function wfTplGetLocalTemplates(){
    try{
      if (typeof Utils === 'undefined') return [];
      const stored = Utils.getFromStorage(WF_LOCAL_TEMPLATES_KEY);
      return Array.isArray(stored) ? stored : [];
    }catch(_){ return []; }
  }
  function wfTplSaveLocalTemplates(list){
    try{ if (typeof Utils !== 'undefined') Utils.saveToStorage(WF_LOCAL_TEMPLATES_KEY, list); }catch(_){ }
  }
  async function wfTplResolve(name){
    if (!name) return null;
    if (name.startsWith('local::')){
      const actual = name.slice('local::'.length);
      const locals = wfTplGetLocalTemplates();
      const found = locals.find(t=>t.name === actual);
      if (!found){
        Toast.error(`Local template "${actual}" not found`);
        return null;
      }
      try{
        return typeof structuredClone === 'function' ? structuredClone(found.body) : JSON.parse(JSON.stringify(found.body));
      }catch(e){
        return JSON.parse(JSON.stringify(found.body));
      }
    }
    return await apiClient.makeRequest('GET', `/api/v1/workflows/templates/${encodeURIComponent(name)}`);
  }
  function wfTplSaveAsNew(){
    try{
      const textarea = document.getElementById('wfDef_payload');
      if (!textarea){ Toast.error('Payload area unavailable'); return; }
      let parsed;
      try{ parsed = JSON.parse(textarea.value); }
      catch(e){ Toast.error('Template JSON invalid: '+(e?.message||e)); return; }
      const suggested = parsed && parsed.name ? String(parsed.name) : '';
      const name = prompt('Name for local template', suggested || 'custom_template');
      if (!name) return;
      const trimmed = name.trim();
      if (!trimmed){ Toast.error('Name required'); return; }
      let locals = wfTplGetLocalTemplates();
      const existing = locals.find(t=>t.name === trimmed);
      if (existing && !confirm(`Replace existing local template "${trimmed}"?`)){
        return;
      }
      locals = locals.filter(t=>t.name !== trimmed);
      const clone = typeof structuredClone === 'function' ? structuredClone(parsed) : JSON.parse(JSON.stringify(parsed));
      locals.push({ name: trimmed, body: clone });
      wfTplSaveLocalTemplates(locals);
      Toast.success(`Saved local template "${trimmed}"`);
      try{ if (typeof Utils !== 'undefined') Utils.saveToStorage('workflows-last-template', `local::${trimmed}`); }catch(_){ }
      wfTplLoadList();
    }catch(e){ Toast.error('Save failed: '+(e?.message||e)); }
  }
  function wfTplDeleteLocal(){
    try{
      const sel = document.getElementById('wfTpl_select');
      if (!sel){ return; }
      const value = sel.value;
      if (!value || !value.startsWith('local::')){
        Toast.error('Select a local template to delete');
        return;
      }
      const actual = value.slice('local::'.length);
      if (!confirm(`Delete local template "${actual}"?`)){ return; }
      const locals = wfTplGetLocalTemplates().filter(t=>t.name !== actual);
      wfTplSaveLocalTemplates(locals);
      Toast.success(`Deleted local template "${actual}"`);
      try{ if (typeof Utils !== 'undefined'){ const saved = Utils.getFromStorage('workflows-last-template'); if (saved === value){ Utils.saveToStorage('workflows-last-template', ''); } } }catch(_){ }
      wfTplLoadList();
    }catch(e){ Toast.error('Delete failed: '+(e?.message||e)); }
  }
  async function wfTplCreateAndRun(tpl, watch){
    const created = await apiClient.makeRequest('POST','/api/v1/workflows',{ body: tpl });
    const wid = created?.id;
    if (!wid){ throw new Error('Create did not return id'); }
    const inputs = (tpl && typeof tpl.default_inputs==='object' && tpl.default_inputs) ? tpl.default_inputs : {};
    const run = await apiClient.makeRequest('POST',`/api/v1/workflows/${encodeURIComponent(wid)}/run?mode=async`,{ body: { inputs } });
    document.getElementById('wfDef_result').textContent = JSON.stringify({ created, run }, null, 2);
    const runId = run?.run_id || '';
    if (watch){
      wfGoToRun(runId);
      setTimeout(()=>{ try{ wfStartWatchStatus(runId); }catch(_){ } }, 350);
    } else {
      const toastEl = Toast.success('Run started: '+ runId, 8000);
      try{
        const btn = document.createElement('button');
        btn.className = 'api-button btn-sm';
        btn.style.marginLeft = '8px';
        btn.textContent = 'Open in Runs';
        btn.onclick = () => { try{ wfGoToRun(runId); }catch(_){} };
        const content = toastEl?.querySelector?.('.toast-content .toast-message') || toastEl?.querySelector?.('.toast-content') || toastEl;
        if (content) content.appendChild(btn);
      }catch(_){ }
    }
    return { created, run, runId };
  }
  try{ wfEnsureFiltersKey(); }catch(_){ }
  try{ wfRestoreTailPreference(); }catch(_){ }
  // Auto-restore Runs filters when Runs tab becomes active
  (function(){
    try{
      const target = document.getElementById('tabWorkflowsRuns');
      if (!target) return;
      const trigger = () => { try { wfRestoreFilters(); } catch(e) { /* ignore */ } };
      if (target.classList.contains('active')) { trigger(); }
      const obs = new MutationObserver((muts)=>{
        for (const m of muts){
          if (m.attributeName === 'class' && target.classList.contains('active')){
            trigger();
          }
        }
      });
      obs.observe(target, { attributes: true });
      window.addEventListener('beforeunload', ()=>{ try{ obs.disconnect(); }catch(_){} });
    }catch(e){ /* ignore */ }
  })();
</script>
